ПРОГРАММИРОВАНИЕ НА VISUAL C++

АРХИВ

РАССЫЛКА ЯВЛЯЕТСЯ ЧАСТЬЮ ПРОЕКТА RSDN , НА САЙТЕ КОТОРОГО ВСЕГДА МОЖНО НАЙТИ ВСЮ НЕОБХОДИМУЮ РАЗРАБОТЧИКУ ИНФОРМАЦИЮ, СТАТЬИ, ФОРУМЫ, РЕСУРСЫ, ПОЛНЫЙ АРХИВ ПРЕДЫДУЩИХ ВЫПУСКОВ РАССЫЛКИ И МНОГОЕ ДРУГОЕ.


No.1

/*____________________________________________________________________

  Программирование на Visual C++

  выпуск No. 1

  ____________________________________________________________________
*/

Добрый день всем!

Итак, свершилось : вы видите перед собой первый выпуск рассылки.

Я  понимаю, что ведение такой рассылки - это огромная ответственность,
принимая  во внимание огромное количество сайтов по этой теме (кстати,
именно  вопиющее  отсутствие  рассылки  о  Visual  C++  на  горкоте  и
подвигнуло  меня на ее создание). Это мой первый опыт такого рода, так
что  пока  я прошу вас не судить слишком строго. Я же со своей стороны
постараюсь   сделать  все  от  меня  зависящее,  чтобы  рассылка  была
интересной, занимательной и полезной.

Прежде  всего,  я хотел бы уточнить тематику наших бесед. А именно то,
что   вы   здесь   не   найдете   учебника  по  программированию  типа
"шаг-за-шагом".  По  этому принципу построено очень много разных книг,
сайтов  в  Рунете  и  других источников. На мой взгляд, такие учебники
учат  только тому, что в них заложено. Они предлагают решение какой-то
задачи, но если вам нужно решить задачу немного отличную от описанной,
то   начинаются  проблемы.  Ведь  очень  часто  авторы  для  упрощения
изложения  выбирают  самые  тривиальные  решения,  не  подходящие  для
большинства реальных задач. И приходится выкручиваться своими силами -
штудируя  help,  делая  методом "тыка". И, хочу заметить, гораздо чаще
такие   вот  искания  оказываются  полезнее  тупого  выполнения  шагов
учебника,  потому что в процессе поиска выдумаетеирешаетесами.

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

Именно поэтому в рассылке не будет учебников типа "пишем свою записную
книжку".

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

Итак, что же будет освещено в рассылке?

Во-первых,  некоторые теоретические сведения - знание теории никогда и
никому не вредило. Причем основной упор будет делаться вовсе не на С++
как  язык  программирования  (хотя это я тоже не исключаю), а на такие
вещи, как MFC и WinAPI.
Во-вторых,  разные полезные приемы программирования и хитрости, советы
и трюки.
Еще, объяснение некоторых английских терминов по программированию.
Потом,  обзор некоторых книг по данной теме, перевод интересных статей
из интернета и, конечно же, ваши письма, вопросы, мнения.

Но этим я не хотел бы жестко ограничиваться. Пишите, что интересно Вам
лично?

//////////////////////////////////////////////////////////////////////

Я долго думал, с чего лучше всего будет начать. Ведь уровень у каждого
из  вас  совершенно  разный.  И  решил, что для начала это должно быть
что-то  "легкое",  понятное  всем,  но и не совсем бесполезное в то же
время.

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

Пробовали ли Вы хоть раз менять стандартную синтаксическую подсветку в
Visual  C++  IDE?  Если  да, то знаете, что, к сожалению, в настройках
доступно только 16 возможных цветов. Какая жалость! Ведь можно было бы
сделать   очень   приятную   цветовую   схему,  ярко  выражающую  вашу
индивидуальность... ну или просто более приятную для глаз.

Неужели  ничего  нельзя  поделать?  Оказывается, можно! Для исполнения
этого  желания  нам  придется  воспользоваться стандартным виртуальным
"ломом"  для Windows. Нет, я не имею ввиду дизассемблирование - избави
бог!  ;)  В  качестве  лома  в  данном  случае  будет выступать просто
редактор реестра (regedit.exe).

Запустите RegEdit и откройте ветвь
HKEY_CURRENT_USER\
Software\Microsoft\DevStudio\6.0\Format\Source Window

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

Вот  и  все  -  теперь  для  любого  элемента программы ( комментария,
строки, ключевого слова или др.) вы можете назначить любой из 16 млн.
доступных цветов!

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

Q. Как изменить стили окон, создаваемых MFC AppWizard'ом по умолчанию?

A. Чтобы  изменить  стиль  по  умолчанию  какого-нибудь  окна,  нужно
перекрыть виртуальную функцию PreCreateWindow() класса этого окна. Эта
функция позволяет приложению получить доступ к процессу созданию окна,
который  по  умолчанию  происходит  в  недрах  MFC  с  помощью  класса
CDocTemplate.  Библиотека  вызывает  PreCreateWindow() перед созданием
окна.  Этой  функции  передается  параметр  -  указатель  на структуру
CREATESTRUCT.  Путем  изменения членов этой структуры вы можете влиять
на стиль создаваемого окна.

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

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

А.  Увы,  это не так элементарно делается, как, скажем, в С++ Builder.
Но  и  здесь  есть  свои плюсы - вы получаете больший контроль. Пусть,
скажем,  у  Вас  есть  кнопка,  которую Вам нужно выровнять по правому
краю,  и соответствующая переменная m_Btn типа СButton в классе вашего
окна или вида. Тогда в функции обработки сообщения WM_SIZE- OnSize()..

void CMyView::OnSize(UINT nType, int cx, int cy)
{
   CFormView::OnSize(nType, cx, cy);
   .
   .
   // ... добавьте вот это:

   if (::IsWindow(m_Btn.m_hWnd))           // условие на корректность

        m_Btn.MoveWindow ( cx - BtnWidth - 10, BtnY,
                           cx - 10, BtnY + BtnHeight,
                           0 );                     // двигаем кнопку

   // конец кода для добавления.
   .
   .
}

Здесь  вместо  BtnY  вставьте желаемую Y-координату кнопки, BtnWidth и
BtnHeight - соответственно целевые ширина и высота кнопки.

Параметр  cx,  передаваемый  в функцию - это новая ширина окна. Данный
код изменяет положение кнопки, чтобы она оставалась ширины Btn_Width и
отстояла  от  правого  края  окна  на  10 единиц. Функция MoveWindow()
меняет размер и положение кнопки. Если вы не знаете BtnY|Width|Height,
то  их  можно определить с помощью функции m_btn.GetClientRect(), ведь
любой элемент управления - это, в принципе, тоже окно.

Выравнивание по нижнему краю производится аналогично, просто по смыслу
меняются параметры MoveWindow().

//////////////////////////////////////////////////////////////////////

Ну   вот,  на  сегодня  пока  все.  Жду  ваших  писем  с  замечаниями,
предложениями и пожеланиями. До скорого!

(С)2000 by Алекс Jenter        mailto:jenter@mail.ru.

http://subscribe.ru/
E-mail:ask@subscribe.ruRelayed byCorbina
РейтингуетсяSpyLog


No.2

/*____________________________________________________________________

  Программирование на Visual C++

  выпуск No. 2
                                                            20/6/2000
  ____________________________________________________________________
*/

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

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

Сегодня  мы  немного  поговорим  об устройстве MFC, а также рассмотрим
один интересный вопрос.

/// НЕМНОГО ТЕОРИИ ///////////////////////////////////////////////////

Как  известно,  основой  всех  основ  в  MFC  является  класс CObject.
Основным  назначением  этого  класса является предоставление некоторых
базовых  возможностей  всем  своим  наследникам,  а  именно  доступ  к
информации о классе во время выполнения и поддержка сериализации, т.е.
сохраняемости объектов.

Однако  уровень предоставляемых возможностей варьируется в зависимости
от  вашего  выбора;  он  зависит  от  включения  определенных макросов
объявления  и  реализации  при создании классов - наследников CObject.
Без  сомнения, вы с этими макросами уже сталкивались, например в коде,
который  генерируют  Wizard'ы.  Пришла  пора  разобраться с ними более
детально.

Итак,  на  характер  вашего класса, производного от CObject, вы можете
влиять  с  помощью  нескольких  макросов. Существуют определенные пары
макросов   -  один  включается  в  объявление  класса  (имеет  префикс
DECLARE_), а соответствующий ему - в реализацию (префикс IMPLEMENT_).

Первая  пара  макросов  -  это  DECLARE_DYNAMIC|IMPLEMENT_DYNAMIC.   С
помощью включения этих макросов в код вашего класса вы можете включить
одну  из  базовых функций CObject - способность узнавать класс объекта
прямо  во время выполнения программы. Для этого вы можете пользоваться
функцией   IsKindOf()  в  связке  с  макросом  RUNTIME_CLASS,  который
возвращает  указатель  на  структуру  CRuntimeClass ( где хранится вся
информация о классе: имя, размер, версия, информация о базовом классе,
указатель на конструктор объекта и т.д.)

Следующая   пара  -  DECLARE_DYNCREATE|IMPLEMENT_DYNCREATE  аналогична
первой,  но к возможности получать информацию о классе добавляется еще
и  возможность создавать объекты этого класса во время выполнения.
Объект создается функцией CreateObject структуры CRuntimeClass. Вот
пример:

 CRunTimeClasspClass = RUNTIME_CLASS(СMyObject);
                          // получаем ук-ль на структуру CRunTimeClass

 CObjectpNewObject= pClass->CreateObject();
                                // создаем новый объект нужного класса

 ASSERT(pNewObject->IsKindOf(RUNTIME_CLASS(CMyObject));
                                            // проверяем класс объекта

И,  наконец,  мы  подошли  к  последней  паре макросов DECLARE_SERIAL|
IMPLEMENT_SERIAL.  Преобразование в последовательную форму и обратно -
сериализация  -  дает  программисту  возможность  сохранения и восста-
новления  объектов. Для того, чтобы воспользоваться этой возможностью,
в  классе-наследнике  нужно перекрыть виртуальную функцию Serialize().
Из  нее  обязательно нужно сначала вызвать родительскую версию. Одна и
та   же   функция   используется   как   для  сохранения,  так  и  для
восстановления   объекта.   Какую   операцию   нужно  произвести,  она
определяет  из  своего  единственного параметра ar типа CArchive . Вот
пример:

 void CMyObject::Serialize( CArchive  ar )
  {
    CObject::Serialize( ar );  // вызываем версию базового класса

      if( ar.IsStoring() )   // если сохраняем,
      {
        ar << something;        // то сохранить что-то
      }
      else                  // а иначе
      {
        ar >> something;        // восстановить
      }
  }

Заметьте,   что  DECLARE_SERIAL|IMPLEMENT_SERIAL  помимо  сериализации
включают  и  те  возможности,  которые  дают  две  первые  пары  - это
естественно,  ведь если вы восстанавливаете объект, то вам понадобится
возможность  создать  его  во  время  выполнения  программы. Например,
приложению  нужно сохранять и восстанавливать некоторый набор объектов
различного  типа.  А  для  вызова  соответствующего  конструктора  при
восстановлении  объекта  нужно  знать  его  тип. Механизм сериализации
сохраняет  информацию  об  объекте  вместе  с  теми  данными,  что  вы
записываете явно в функции Serialize().

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

Следующий вопрос поступил от одного из подписчиков:

Q При  программировании   элементов   ActiveX, в этой технологии есть
возможность  структурного  хранения  данных  на диске, т.е. создание в
файле  так  называемых  хранилищ  и потоков (использование интерфейсов
IStream  и IStorage), проще говоря - представление файла данных в виде
иерархической  системы  внутренних  каталогов и файлов, которые там (в
данном  файле  данных)  имеют  свои  строковые  имена.  Есть  ли в MFC
возможность  структурного  хранения,  скажем,  используя объект класса
CArchive   в   переопределяемой  функции  Serialize(CArchive)  класса,
производного  от  CDocument,  ну  и  так  далее?  Конечно, этого можно
добиться,  создав свои собственные наработки (а как хороша эта идея, я
имею  в  виду  использование  потоков и хранилищ), но все таки хочется
знать, есть ли такие возможности в MFC, чтобы не тратить зря время.
                                      - Броник  (krivoruchko@nvrsk.ru)

A Насколько  мне  известно, поддержки такой иерархической системы в
MFC нет. Во всяком случае, я ее не обнаружил. CArchive и Serialize для
этой   цели   явно   не   предназначены:  в  них  важную  роль  играет
последовательность  записи, т.е. в каком порядке вы что-то записали, в
таком  нужно  это  и прочитать. Так что, скорее всего, придется писать
свой  класс  для  этой  цели. Конечно, это не слишком обнадеживает, но
зато   этот   класс   можно  будет  использовать  во  всех  дальнейших
программах,  где  потребуется  такая  форма хранения данных. Или - как
вариант  - можно сделать просто класс-обертку для интерфейсов IStorage
и  IStream.  Конечно, в этом случае придется подключать библиотеку COM
(которую   в   MFC-приложениях,   в   принципе,   никто  не  запрещает
использовать).  Впрочем,  если  кто  знает  что-нибудь о существовании
такого механизма в MFC - пожалуйста, поделитесь с нами.

//////////////////////////////////////////////////////////////////////

Всего наилучшего и чтоб программы ваши не знали ошибок.

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

 .

http://subscribe.ru/
E-mail:ask@subscribe.ruRelayed byCorbina
РейтингуетсяSpyLog


No.3

/ *

Программирование на Visual C++

выпуск № 3 от 23/06/2000

* /

Здравствуйте!

Да, это должно было произойти и это произошло! Рассылка получила официальный статус "обычной некоммерческой рассылки" (причем гораздо быстрее, чем я ожидал), с чем я себя и всех вас и поздравляю!

Хочу извиниться перед подписчиками HTML-версии: во втором выпуске случилось некоторое искажение исходного кода (я упустил из виду, что при автоматической генерации HTML сервер Гор. Кота звездочки (*) интерпретирует как указание сделать шрифт жирным), в результате чего были потеряны указатели. Вот как этот код должен был выглядеть на самом деле:

 CRunTimeClass *pClass = RUNTIME_CLASS(СMyObject);
             // получаем ук-ль на структуру CRunTimeClass

 CObject *pNewObject = pClass->CreateObject();
                 // создаем новый объект нужного класса

 ASSERT(pNewObject->IsKindOf(RUNTIME_CLASS(CMyObject));
                            // проверяем класс объекта

Меня удивило, что большинство из вас подписывается именно на HTML - я думал, наши люди как никакие другие считают каждый килобайт. Но, видимо, времена меняются - в лучшую сторону. Дай бог! Так что по вышеописанным причинам я решил поднапрячься и сработать собственный HTML-вариант. То, что получилось, сейчас перед вами, уважаемые HTML-подписчики. Тех, кто выписывает текстовый вариант, уговаривать подписаться на HTML я не буду, потому что прекрасно их понимаю ;) Оба варианта я буду делать лично, никакой автоматической генерации. Enjoy.

/ / / / ОБРАТНАЯ СВЯЗЬ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Мне пришло интересное письмо на тему предыдущего выпуска. Хочу предложить его вашему вниманию:

Приветствую!
Я только что обнаружил эту рассылку, подписался и 2 первых выпуска прочитал в архиве. Очень надеюсь, что смогу оказать посильную помощь автору и читателям рассылки, так как около 30 лет занимаюсь программированием и последние 3-4 года - Visual C++. На работе сейчас я программирую именно на этом [языке - AJ], Visual Studio 6.0
В отношении вопроса Броника. Конечно, система сериализации для ActiveX имеется. Для этого рекомендуется использовать класс COLEControl (порожденный из CWnd ->CCMDTarget->CObject).
Вот пример из MSDN (у меня есть этот хелп и на работе и дома)

void CMyCtrl::Serialize(CArchive& ar)
{
    DWORD dwVersion =
        SerializeVersion(ar, MAKELONG(_wVerMinor, _wVerMajor));
    SerializeExtent(ar);
    SerializeStockProps(ar);

    if (ar.IsLoading())
    {
        // загрузить свойства
    }
    else
    {
        // сохранить свойства
    }
}

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

IMPLEMENT_SERIAL( CMyClass, CObject, VERSIONABLE_SCHEMA | 2 )
// 2- Номер текущей (новой) версии. 1 - номер старой версии

void CMyClass::Serialize(CArchive& ar)
{
    if (ar.IsLoading())
    {
         UINT Version;
         ar.ReadClass( RUNTIME_CLASS(CMyClass), &Version);
         switch(Version)
         {
           case 2:
              // чтение по-новому
              break;
           case 1:
              // чтение по-старому
              break;
         }
    }
    if (ar.IsStoring())
    {
        ar.WriteClass( RUNTIME_CLASS(CMyClass));
                 // запись по-новому
    }
}

Понятное дело, при продвижении версии номер в IMPLEMENT_SERIAL возрастает, и добавляется новый
случай case только при чтении соответствующих версий.
Еще одно важное замечание. При использовании механизма сериализации мы платим некоторую цену: не допускается никаких абстрактных классов - забудьте, что это существует!

- Boris Berdichevski

Что ж, огромное спасибо Борису за комментарии и дополнения. Я надеюсь, он и в будущем будет нам посильно помогать. Что касается вопроса Броника - думаю, он все-таки спрашивал не о том, как сделать сериализацию для ActiveX (хотя это тоже очень интересный момент), а как организовать структурированное хранение данных в файле, наподобие того, что присутствует в ActiveX. Ответа на этот вопрос, за исключением предложенного мной в предыдущем выпуске, пока нет.

Просьба: когда пишете мне, пожалуйста оговаривайте ваше отношение к публикации вашего e-mail адреса. Я оставляю за собой право решать, какие из ваших писем появятся в рассылке. По умолчанию адрес публиковаться не будет. Если вы хотите связаться с человеком, письмо которого вы прочитали в рассылке, но чей адрес не был указан, пишите мне с пометкой в subject'e для кого это письмо.

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

Q. Идея рассылки и её тематика очень понравились, даже добавлять или изменять ничего не хочется, как по заказу. И даже уже вопрос созрел. При первом знакомстве с MFC помню была одна проблема. Никак не получалось сменить пиктограмку курсора во время выполнения программы. Т.е. последовательность стандартных действий LoadCursor и SetCursor не срабатывала, хотя при создании окна этих действий хватало. В связи с этим вопрос: Какие ещё действия надо выполнить для смены пиктограмки курсора
во время работы приложения. Сейчас, к сожалению, интересы лежат не в области С++ и MFC. Поэтому на разрешение вопроса своими силами просто нет времени.

- softmax

A. Спасибо за добрые слова о рассылке. По вопросу - проблема здесь в том, что система автоматически при каждом движении мыши восстанавливает тот курсор, который был указан при регистрации класса окна. Вообще я знаю три способа изменить курсор в MFC-приложении, причем два из них имеют некоторые ограничения: один используется, в самом деле, при создании окна, а второй работает только с одним курсором - стандартными песочными часами. Думаю, что стоит описать все три способа, для того, чтобы вы могли выбрать наиболее для вас подходящий. Итак:

Способ №1 (универсальный). Нужно перекрыть функцию OnSetCursor() класса CWnd, родителя вашего окна (вида). В ней необходимо сообщение обработать самому, устанавливая нужный курсор. Для тех, кто не знает, сообщение WM_SETCURSOR посылается окну тогда, когда курсор мыши двигается внутри окна, причем мышь приложением не захвачена (с помощью функции SetCapture()). Вот пример из MSDN:

BOOL CMyView::OnSetCursor(CWnd* pWnd,
                             UINT nHitTest, UINT message)
{
  if ( m_ChangeCursor )
  {
      // устанавливаем стандартный курсор вида "I"
     ::SetCursor(AfxGetApp()->LoadStandardCursor(IDC_IBEAM));
     return TRUE;
  }

  return CView::OnSetCursor(pWnd, nHitTest, message);
}

Конечно, можно установить и ваш собственный курсор, только вместо LoadStandardCursor() нужно будет воспользоваться LoadCursor() или LoadOEMCursor(). С помощью параметра nHitTest можно определить область, в которой сейчас находится курсор. Вообще, этот способ лучше применять только тогда, когда вам в самом деле нужно динамически менять один курсор на другой (причем отличный от песочных часов), потому этот способ самый нерациональный (прикиньте-ка. сколько раз будет выполняться этот обработчик). Лучше все нужные курсоры загрузить заранее, а из функции- обработчика Load..Cursor() не вызывать. Хотя, в принципе, я для примера сделал такой обработчик - никакой разницы в скорости не заметил...но это уже зависит от конкретного компьютера, наверное. И потом, наверное не стали бы в MSDN это советовать, если бы не знали, что делали ;)

Ну а тем, кому лишь надо видом курсора показать пользователю, что компьютер сейчас занят какой-то операцией, идеально подходит

Способ №2 (песочные часы). Этот способ самый простой. Вызывайте функцию BeginWaitCursor()
перед началом операции и EndWaitCursor() после ее завершения. Единственный нюанс здесь в том, что если эти два вызова должны находиться в разных функциях-обработчиках, то вам все же придется перекрыть OnSetCursor(), причем это выглядит примерно так:

BOOL CMyView::OnSetCursor(CWnd* pWnd,
                            UINT nHitTest, UINT message)
{
  if (m_ChangeCursor)
  {
    RestoreWaitCursor();// восстанавливаем курсор-пес.часы
    return TRUE;
  }

  return CView::OnSetCursor(pWnd, nHitTest, message);
}

В этом случае перед вызовом BeginWaitCursor() m_ChangeCursor нужно приравнять к TRUE, а после EndWaitCursor() - к FALSE.

Способ №3 (класс окна). Этот метод применяется, когда вам для какого-то окна нужно установить конкретный курсор, причем желательно на все время существования окна. Перекрываете PreCreateWindow() и регистрируете свой класс окна, изменяя поле lpszClass параметра cs типа CREATESTRUCT:

BOOL CMyView::PreCreateWindow(CREATESTRUCT& cs)
{
  cs.lpszClass = AfxRegisterWndClass(
     CS_DBLCLKS | CS_HREDRAW | CS_VREDRAW, //стили окна
     AfxGetApp()->LoadCursor(IDC_MYCURSOR),// курсор
     (HBRUSH) (COLOR_WINDOW + 1));    // цвет фона окна

  return CView::PreCreateWindow(cs)
}

В качестве первого параметра для AfxRegisterWndClass() можно указать "cs.style", чтобы установить стиль окна по умолчанию.

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Ну вот и все на сегодня. Удачного вам программирования.

©Алекс Jenter mailto:jenter@mail.ru
 Красноярск, 2000.


No.4

/ *

Программирование на Visual C++

выпуск No. 4 от 25/06/2000

* /

Добрый день, уважаемые подписчики!

Мне приходит очень много ваших писем с вопросами, советами, комментариями, предложениями и т.д. Как повелось, самые интересные из них я публикую в рубриках "Обратная связь" и "Вопрос-ответ". Но, как вы наверное сами понимаете, я просто физически не в состоянии отвечать на такое количество вопросов, касающихся программирования. Я отвечаю быстро, когда сразу знаю ответ, но ведь чаще приходится самому сидеть и разбираться, а на это уходит много времени. От этого в первую очередь страдают другие рубрики рассылки - я не успеваю подготовить хороший материал для вас (кстати, я как раз хочу сделать несколько новых рубрик... но пусть лучше это будет сюрприз). Моя задача как автора рассылки состоит в том, чтобы готовить и публиковать интересную информацию по обозначенной тематике. Я же в последнее время больше сам занимаюсь программированием. Для меня, это, конечно, полезно ;) но вот как насчет вас? На многие вопросы я уже ответил, хотя они не появятся в рассылке, т.к. довольно узко направлены и вряд ли интересны для "широкой общественности". Ответы на другие или слишком обширны и тянут на тему отдельного выпуска, или попросту элементарны.

Решив, что так дальше дело все-таки не пойдет, я придумал новую схему. Мне пришло несколько писем с предложениями помощи от программистов, выписывающих рассылку (огромное спасибо им!), так что теперь лишь на некоторые (самые интересные ;) вопросы я буду отвечать лично (в рубрике "Вопрос-ответ"), а другие просто опубликую отдельно. В дальнейшем эти вопросы (вкратце) уже вместе с ответами на них появятся в рубрике "Вопрос-ответ" (мне, кстати, предлагали расширить эту рубрику - вот хороший повод). Я надеюсь, наше с вами мнение совпадет и вы тоже посчитаете, что так будет лучше. Еще я очень полагаюсь на ваше сотрудничество - если знаете ответ на вопрос, не поленитесь и напишите! Человек будет вам благодарен, да и не только он, а все читатели, которые узнают что-то новое. А я лично преобладающую свою роль программиста-консультанта сменю на роль ведущего рассылки.

С другой стороны, я ни в коем случае не хочу рассылку превращать в тривиальную дискуссионную группу. Будет несколько новых рубрик! Поэтому количество вопросов, рассматриваемых в выпуске, будет ограничиваться (в среднем) двумя-тремя вопросами в рубрике "Вопрос-ответ" плюс два-три вопроса, ожидающие ответа.

Кстати, могу дать очень хороший совет для тех, кто отчаялся найти решение своей проблемы: попробуйте, действительно, задать вопрос в дискуссионной группе, например на news.microsoft.com, в microsoft.public.ru.vc или microsoft.public.russian.programming, а если знаете английский, лучше в одну из многочисленных microsoft.public.vc.* Там отвечают действительно быстро, сам не раз прибегал к этому! И не забудьте порекомендовать подписаться на рассылку :)

Очень многие спрашивают, где можно прочитать предыдущие выпуски рассылки. Отвечу лучше здесь: смотрите на http://subscribe.ru/archive/comp.prog.visualc/

Да, и еще. Выпуски, скорее всего, станут выходить чаще, до 3-4 раз в неделю, иначе размер каждого заметно увеличится. А я сам знаю, как неприятно забирать 30-40 Кб письма, когда постоянно рвется связь (да простят меня читатели с хорошей связью;) В каждом выпуске помимо обсуждений и вопросов я постараюсь публиковать что-то интересное. Have fun.

/ / / / ОБРАТНАЯ СВЯЗЬ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

По поводу вопроса смены курсора в MFC-приложении (выпуск No.3) пришло несколько дополнений и замечаний (некоторые несущественные моменты я опустил, вместо них поставил знак "[...]", свои комментарии в теле письма я также привожу в квадратных скобках):

Подобно третьему случаю, но не только в PreCreateWindow()

В тот момент, когда хотим поменять курсор (для всего приложения) проделываем следующие действия:

::SetClassLong( theApp.m_pMainWnd->m_hWnd,
                GCL_HCURSOR,
              ::LoadCursor(theApp.m_hInstance,
               MAKEINTRESOURCE(IDC_MY_CURSOR)));

Немного длинная строчка кода :)) [...]
Как обычно встречается такая проблема: theApp, хоть и объявлена глобально (объявляется если проект создается стандартным Wizard-ом), не видна в других файлах проекта. Чтобы обойти это просто в файле объявления класса вашего приложения (производного от CWinApp) сразу после объявления класса добавьте следующую строчку:

extern CMyApp theApp;

где CMyApp - имя класса вашего приложения.

- Andrew Gromyko

Замечу, что theApp так объявлять необязательно, так как многие, и я в том числе, пользуются вместо этого стандартными функциями MFC AfxGetApp() и AfxGetMainWnd(), которые возвращают указатель на объект-приложение и главное окно соответственно.
Вот еще одно письмо на эту тему:

Проблема смены курсора меня настигла еще на раннем этапе моего программистского становления. Поэтому хочу предложить тот вариант, который является по-моему наиболее адекватно подходящим с точки зрения скорости и идеологии Win32 API. Способ три конечно хорош и его можно расширить на все время существования окна. Вот как происходит вызов в классе окна:

::SetClassLong (m_hWnd,GCL_HCURSOR,
     (long)AfxGetApp()->LoadCursor (IDC_MY_CURSOR));

При этом курсор меняется перманентно, т.е. например при вызове SetCursor() вид курсора меняется при начале движения мыши. А так меняется просто класс окна. Работает быстро и надежно. [...]

- Alex (seaside@i.com.ua)

Спасибо Андрею и Алексу за это дополнение, теперь мы с вами знаем четыре способа изменить курсор;). Должен признаться, в предыдущем выпуске я допустил оплошность, ляпнув что первый параметр у AfxRegisterWndClass() можно заменить на cs.style, причем, как назло, понял свою ошибку как раз в тот момент, когда отправлял рассылку "в эфир". Мне не преминули на эту ошибку указать, и я очень рад этому. Пожалуйста, будьте бдительны!!! Я тоже человек! Подробности читайте ниже...

Спасибо Вам за создание действительно нужной рассылки. [Мерси за комплимент ;) - AJ]
Я не так давно начал использовать MFC, и информация, ответы на вопросы мне очень пригодятся. Интересно, что уже в первом прочитанном мной выпуске обсуждался вопрос о курсоре, решение которого я буквально только что искал сам. Поэтому я решил написать этот отзыв в виде нескольких замечаний. На всякий случай оговорю, что все ниже написанное не более чем мое humble opinion :-))

1) Мне не кажется, что описанный Вами универсальный способ изменения курсора нерационален. Не этот обработчик, так CWnd::OnSetCursor() все равно вызывается при каждом движении мыши. Поэтому разнице в скорости практически неоткуда взяться. Хотя, LoadCursor() действительно лучше вызвать один раз (когда этот курсор впервые устанавливается), сохранить дескриптор нового курсора, который и передавать системной функции SetCursor() в обработчике. Мне кажется, это будет по сути то же самое, что делается при обработке события WM_SETCURSOR по умолчанию.

2) О "песочных часах". Если операция, на время которой высвечиваются часики, относительно невелика по времени, и при ее выполнении приложение может не реагировать на другие события, проще всего выделить блок обработки и определить в этом блоке локальный объект CWaitCursor. Все необходимые операции по установке и удалению курсора будут сделаны конструктором и деструктором этого объекта. Именно так, например, в MFC реализованы часики при открытии и сохранении документа. Если же во время операции система реагирует на другие события, то удобнее применять Begin/Restore/EndWaitCursor() [Итак, способов уже пять! - AJ]

3) Вы показываете пример с переопределением PreCreateWindow(), в котором регистрируется новый класс окна, и в конце пишете: "В качестве первого параметра для AfxRegisterWndClass() можно указать "cs.style", чтобы установить стиль окна по умолчанию." По-моему, это некорректно. Ведь cs.style есть комбинация стилей для окна, а не для класса окна, что требуется при регистрации класса. [Виновен, Ваша честь! - AJ] Мне пришлось столкнуться с этой проблемой, когда я хотел для своего класса CMyView добавить стиль CS_OWNDC, не меняя ничего больше. Дело в том, что при самом первом вызове CMyView::PreCreateWindow() класс окна просмотра еще не существует, так как он регистрируется в CView::PreCreateWindow(). Поэтому пришлось вызывать родительскую функцию дважды. Вот мое решение, которое, быть может, будет кому-то полезно:

//h-файл

class CMyView :public CView
{
  public:
   virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
  protected:
   static LPCTSTR lpszViewClassName;
}

//cpp-файл

LPCTSTR CMyView::lpszViewClassName = NULL;

BOOL CMyView::PreCreateWindow(CREATESTRUCT& cs)
{
 if (lpszViewClassName == NULL)
 {
  CView::PreCreateWindow(cs);

  WNDCLASS wc;
  //пытаемся получить информацию о классе
  //если не удается, выходим с ошибкой
  if (!::GetClassInfo(AfxGetInstanceHandle(),
                cs.lpszClass, &wc))
      return FALSE;

  // теперь изменяем в wc все, что нужно
  // например, стиль класса
  wc.style |= CS_OWNDC;
  // регистрируем класс
  lpszViewClassName = AfxRegisterWndClass(
          wc.style, wc.hCursor,
          wc.hbrBackground, wc.hIcon);
 }
 // изменяем класс окна на созданный нами:
 cs.lpszClass = lpszViewClassName;

 return CView::PreCreateWindow(cs);
}

[Я немножко подправил код и добавил комментарии. Надеюсь, автор на меня не обидится - AJ]
Есть у меня и вопросы, ответы на которые, возможно, будут интересны не только мне.

Q1) В приложении есть операция, которая требует, скажем, больше пяти минут времени, причем по некоторым причинам дальнейшее выполнение не может быть продолжено до завершения этой операции. Хотелось бы, чтобы при этом окно приложения нормально обновлялось, могло быть свернуто-развернуто и т.п. Я нашел некоторое решение, но оно требует создания второго цикла обработки сообщений и потому мне не очень нравится, хотелось бы сделать более естественно.

Q2) Можно ли переопределенный обработчик событий сделать подставляемым (inline)?

- Куканов Алексей (ask_atos@mail.ru)

Очень хорошее, обстоятельное письмо. Хотя оно и содержит вопросы, я все же решил поместить его в "обратную связь", т.к. по большей части относится к теме, ну а разбивать письмо на две части - это было бы варварство. Если кто-нибудь знает ответы на заданные вопросы - очень прошу поделиться! Вопросы действительно интересные.

Если у вас есть еще какие-либо комментарии на тему смены курсора мыши, пишите лучше сейчас. После следующего выпуска тема будет считаться закрытой.

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

Вопросы прислал Ivan Pouzyrevsky. И хотя у некоторых из вас они могут вызвать улыбку, я решил опубликовать их в рассылке вместо личного ответа, т.к. все-таки довольно большое число начинающих программистов на VC далеко не сразу понимает, как работать с диалогами в концепции MFC.

Q. Создаю кнопку About. В MFC Class Wizard создаю функцию: IDS_ABOUT->BN_CLICKED. А какой код на открытие окна About?

A. Чтобы вызвать модальный (т.е. не разрешающий переключаться куда-то еще в приложении, пока пользователь не закрыл его) диалог, следует воспользоваться ф-цией DoModal():

      CAboutDlg aboutDlg;
      aboutDlg.DoModal();

AppWizard генерирует такой код сам на команду ID_APP_ABOUT. Так что проще всего, если вы при создании приложения попросили AppWizard создать окно About, назначить Вашей кнопке идентификатор ID_APP_ABOUT. Тогда больше ничего делать не надо.
А вообще, таким образом можно вызвать не только диалоговое окно About, но и любое другое. Кому не совсем ясно, как обеспечить обмен данными с диалогом, присылайте заявки. В случае наличия интереса это станет одной из следующих тем выпуска.
Господа опытные программисты, прошу не тратить силы на ворчание - я не могу угодить всем! Для вас тоже будет кое-что интересное.

Q. У меня в программе при написании слова выполняется функция. Например:

   if(UpperValue==CALCULATOR)
   {
     system("calc.exe");
     m_TestEdit="";
     UpdateData(FALSE);
   }

Но тут вопрос: как пользователю без изменения кода добавлять в базу данных слова?
Например диал. окно
Слово=
Файл Запуска=
 И как указывать путь к файлу, а то при system("D:\UnrealTournament\System\unrealtournament.exe") пишет, что файл не найден?

A. Попробуйте сделать два массива (или списка) CString - один для слов, другой для файлов. Добавляйте в массивы данные по мере ввода их пользователем. При запуске вызывайте нужный файл. Это можно более эффективно сделать с использованием ассоциативного списка (СMap), но сейчас, пожалуй, лучше не забивайте этим голову.

В С++ в строках символ "\" воспринимается как управляющий, чтобы представлять такие вещи, как "\n" - Enter, "\b"- звонок, "\0" - "косой ноль" и др. В частности, управляющая последовательность "\\" сама представляет символ "\", поэтому в строке его надо удвоить, а т.к. используются длинные имена, то лучше еще заключить строку в дополнительные кавычки с помощью управляющей посл-ти \" (хотя у меня работало и без них):

system("\"D:\\UnrealTournament\\System\\unrealtournament.exe\"");

Да, надо сказать, уважаемый Ivan, что мне дико нравится файл, который Вы запускаете ;)

/ / / / В ПОИСКАХ ИСТИНЫ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Ну вот, как я и обещал - новая рубрика. Ваши вопросы, на которые я надеюсь от вас же получить ответы. Ну, поехали:

Q. При вылизывании проекта под MS Visual C++ 5.0 была произведена замена операции заполнения служебных переменных данными из файла на диске на операцию заполнения из строковых ресурсов проекта (файл .rc). Используется ф-ция LoadString() MFC класса CString с использованием в качестве аргумента ф-ции числового идентификатора ресурса (передается не IDS_XXXX, а его числовое значение). Файл "resource.h" в необходимые файлы включен. Под VC++ 6.0 - картина аналогичная :(. Компиляция проекта при этом происходит без ошибок и предупреждений. При выполнении проекта в Debug-версии на этапе выполнения указанной выше ф-ции возникает "Debug Assertion Failed" в файле afxwin1.inl на строке 22. Этот блок из себя представляет следующее:

_AFXWIN_INLINE HINSTANCE AFXAPI AfxGetResourceHandle()
 {
   ASSERT(afxCurrentResourceHandle != NULL);//строка 22
   return afxCurrentResourceHandle;
 }

При нажатии на клавишу "Пропустить" программа идет дальше и вываливается на следующей операции загрузки строки с теми же симптомами и так до тех пор, пока не будут загружены все строки. После этого выполнение программы продолжается в нормальном режиме и все остальное работает как надо (строки все-таки загружаются!). В Release-версии программа пролетает это место без спотыканий.
Ясно, что можно не обращать на это внимание и сделать условную компиляцию, но дело в принципе! Не могу разобраться в чем тут закавыка.

- Евгений

По-моему, вопрос несложный, и имей я лишнее время - разобрался бы сам. Что-то тут с инициализацией, попытка использования раньше времени (как мне кажется)... Но я рассчитываю на тех, кто, прочитав это, воскликнет "ну это ж элементарно!" и сразу начнет писать ответ;)

Те, кто задал вопрос, но пока его не увидел в рассылке и не получил личного ответа - не отчаивайтесь, ждите новых выпусков.

/ / / / АНОНС / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Читайте в следующих выпусках рассылки:

  • Что дядя Билли нам готовит, или Visual Studio Next Generation

  • WinAPI: не запутайтесь в типах

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Ну вот, видите какой большой получился выпуск, хотя в нем, фактически, не было ничего кроме ваших писем. Думаю, это служит доказательством целесообразности нового режима выхода рассылки. Не бойтесь, что рассылка станет просто большой конференцией - я постараюсь этого не допустить! ;) Все хорошо в меру. Все ваши замечания и предложения с благодарностью принимаются.

До новых встреч. Всего хорошего!

©Алекс Jenter mailto:jenter@mail.ru
 Красноярск, 2000.


No.5

/ *

Программирование на Visual C++

выпуск No. 5 от 28/06/2000

* /

Приветствую!

Итак, рассылка снова с вами, уважаемые подписчики, и вы видите сейчас уже пятый выпуск. Сегодня мы поговорим о типах данных и рассмотрим ваши вопросы и ответы.

/ / / / WINAPI / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

WinAPI - это одна из обещанных мной новых рубрик. Как следует из ее названия, в ней мы будем рассматривать вопросы, посвященные Windows API.

 WinAPI: НЕ ЗАПУТАЙТЕСЬ В ТИПАХ

Вы когда-нибудь попадали на страницу Win32 Simple Data Types в Help? В переводе с английского simple означает "простой", т.е. Microsoft хочет сказать, что это "простые типы". В С++ простыми типами были int, double и другие. При программировании для Windows эти типы никуда не деваются, но появляется очень много новых. Они, конечно, не входят в стандарт C++ и не являются его ключевыми словами (ведь на С++ программируют не только под Windows), но любой Windows-программист должен эти типы хорошо знать. И краткого описания типа, приведенного на вышеуказанной странице MSDN, часто бывает недостаточно, так что иногда приходится лезть в исходники и смотреть, что же из себя представляет тип на самом деле. Я вам предлагаю обзор самых основных и важных типов.

Типы Win32 гораздо легче понять, если знать некоторые соглашения. Например, названия типов, по своей природе являющихся указателями, начинается с префикса P или LP. Кстати, LP означает Long Pointer (дальний указатель) и остался в наследие от Windows 3.1, когда указатели еще делились на ближние (содержащие только смещение в сегменте) и дальние (содержащие как сегмент, так и смещение). Префикс H означает HANDLE - это типы, используемые для описания различных объектов, а префикс U - что тип беззнаковый.

С типами INT, UINT, LONG, ULONG, WORD, DWORD, VOID, SHORT, USHORT, CHAR, FLOAT. BYTE, BOOL(BOOLEAN), у вас не должно быть никаких проблем, и было бы глупо их тут расписывать. Эти типы дублируют встроенные типы С++, и единственное, на что здесь нужно обращать пристальное внимание - это размер типа. Эти типы рекомендуется использовать вместо встроенных в С++ для улучшения переносимости приложения, т.к. в разных системах встроенные типы имеют различные размеры.

Очень интересен тип WINAPI. По-хорошему это все-таки не тип. Если вы посмотрите в файл windef.h, то увидите следующую строку: "#define WINAPI __stdcall". __stdcall - это ключевое слово языка С++, оно, в частности, влияет на механизм передачи параметров функции. Суть механизма, определяемого __stdcall состоит в том, что 1) аргументы передаются справа налево; 2) аргументы из стека выбирает вызываемая функция; 3) аргументы передаются по значению (by value), а не по ссылке (by reference), т.е. функции передаются копии переменных; 4) определяет соглашение по декорированию имени функции, т.е. включению в имя дополнительной информации, используемой компоновщиком; 5) регистр символов не изменяется.
То есть оказалось, что WINAPI - это не вовсе тип, а указание о том, что функция использует соглашение __stdcall. Кстати, имейте в виду, что описатель PASCAL и __pascal - это то же самое, что и WINAPI. Но этот описатель является устаревшим, оставлен лишь для совместимости, и Microsoft рекомендует повсеместно использовать вместо него WINAPI.

Использование соглашения __сdecl вместо __stdcall иногда оправданно, но приводит к увеличению размера исполняемого модуля из-за того, что имя функции декорируется в этих соглашениях по-разному.

...продолжение следует...

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

Я очень рад, что мой расчет оказался верным и нашлись знающие люди, готовые ответить на заданные в предыдущем выпуске вопросы. Огромное им спасибо!

Q. В приложении есть операция, которая требует, скажем, больше пяти минут времени, причем по некоторым причинам дальнейшее выполнение не может быть продолжено до завершения этой операции. Хотелось бы, чтобы при этом окно приложения нормально обновлялось, могло быть свернуто- развернуто и т.п.

- Куканов Алексей (ask_atos@mail.ru)

A1. А в чем проблема? Внутри, допустим цикла иногда добавляется цикл:

    for( ; GetMessage(lpMsg, hWnd, 0, 0); DispatchMessage(lpmsg) );

И для красоты на все "запрещенные" действия ставим флаг (который взводим/гасим по необходимости). Вот и весь велосипед.

- Сергей Бойко

Мне вот только не совсем понятно, что значит "иногда добавляется"... Время от времени добавляется, что ли? ;)

A2. Я решил написать ответ на вопрос Куканова Алексея, о корректной прорисовке окна во время какого-то процесса. С MFC это решается элементарно. Пусть есть функция LRESULT Calculation (LPVOID pParam); Не обращайте внимание на параметры объясню позже. Так вот вместо того чтобы в теле какого-то обработчика запускать эту функцию

    CMyDlg::OnButtonClick ()
    {
        Calculation ();
    }

и ждать когда она закончит лучше сделать так

    CMyDlg::OnButtonClick ()
    {
        AfxBeginThread (Calculation,
         (LPVOID)m_hWnd,THREAD_PRIORITY_LOWEST);
    }

По сути MFC запускает параллельную нить, которая никак не влияет на перерисовку всего остального. Обычно можно в качестве pParam передать HWND окна. Потому как узнать когда закончится процесс можно только при помощи сообщений. Например в теле Calculation
::SendMessage ((HWND)pParam,WM_STOP,0,0);
А кто хочет узнать побольше читайте MSDN - "Worker threads".

- Alex (seaside@i.com.ua)

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

A3. Самый оптимальный по-моему способ: Это запустить worker thread - второй поток (если пока только один :)) ) апликации. В качестве параметра передать туда структуру с необходимыми данными, а можно и ничего не передавать. Если все данные хранятся в наследнике CWinApp (дальше - CMyApp) , то получить доступ к объекту апликации можно с помощью функции AfxGetApp(). Единственное замечание по передаче данных из одного потока в другой заключается в том, что надо доступаться ТОЛЬКО к мемберам класса - нельзя вызывать функции класса из другого потока (вернее, можно, если они не изменяют данных класса или не обращаются к оконным функциям класса (относиться к наследникам CWnd)). В итоге имеем схему:
1. Создается worker thread (поток одной функции, при ее завершении завершается и поток). В качестве параметра функции
AfxBeginThread передается указатель на необходимые данные.
2. В основном потоке создается собственное сообщение, сигнализирующее о завершении потока. Оно будет брошено рабочим потоком перед своим завершением с помощью PostMessage (при работе с потоками я предпочитаю PostMessage для обмена такого рода сообщениями, ведь SendMessage ждет завершения работы обработчика события, что часто просто не нужно).
3. Запущенный поток выполняет всю черновую работу, в то время как основной поток апликации занимается важными делами, а именно - ничего не делает, знай крутит себе цикл обработки сообщений и не жужжит.
4. По завершении работы, worker thread посылает основному потоку мессагу, мол я закончил, выкладывает результаты так, чтобы основной знал где они (как это сделать - миллионы способов :)), в частности, передать в завершающем сообщении указатель на данные результата. )
Примерный код таков.

UINT WorkerThreadFunction(WPARAM, LPARAM lpData)
{
// тутачки работаем с lpData и выполняем
// всю необходимую работу
// результат запихиваем в память,
// а адрес на нее - в lpResult
   AfxGetMainWnd()->PostMessage(ID_WORKER_THREAD_END, 0, lpResult);

// возвращаем код успеха (а вообще это на ваш вкус)
    return 0;
}

void CMyApp::OnStartExecution()
{
// заполняем lpData нужными данными, и вызываем ..
    CWinThread *pThread = AfxBeginThread(WorkerThreadFunction, lpData);
    if ( !pThread )
    {
      // Не смогли запустить поток.
      // Правда обычно этот код не выполняется :)).
      // Я до сих пор не знаю ситуации, когда поток
      // может не запуститься, кроме low memory.
      AfxMessageBox(_T("Can't start thread."));
    }
}

LRESULT CMyApp::OnWorkerThreadEnd(WPARAM wParam, LPARAM lpResult)
{
// тутачки обрабатываем завершение расчетов.
}

Замечания:
1. Объявлять обработчик сообщения ID_WORKER_THREAD_END надо через ON_MESSAGE макрос
2. Потоков запускать можно сколько душе угодно (если хватает памяти :)) ).
3. Повторюсь: важно понимать, что доступ к данным может быть одновременным из разных потоков. Поэтому необходима синхронизация. чтобы не получилось ситуации, что один поток пишет, а второй читает в одно время - как результат, можно разрушить логическую целостность данных. Поэтому в приведенных примерах лучше сбрасывать данные в отдельный блок памяти и передавать указатель на него. И функция рабочего потока, и обработчик сообщения завершения должны освобождать передаваемую им память.

- Oleg Tselobyonok, Applied systems, Ltd.

A4. Самым естественным способом решения задачи N1 является создание дополнительного потока в процессе, который собственно и будет выполнять ту самую длительную операцию. С другой стороны - основной поток должен ожидать завершения операции. Для этого в Win32 существует функция
WaitForSingleObject, одним из параметров которой задается описатель ожидаемого потока. Но в этом случае ждущий поток не может обрабатывать
сообщения своего окна, так как ему система перестает выделять процессорное время. Здесь можно придумать много разных способов: во-первых, совсем можно обойтись и без функции WaitForSingleObject, создав глобальную переменную, которая при запуске потока инициализируется в false, а по его завершении - в true (или наоборот); можно, кроме того, используя функцию WaitForSingleObject, задавать ей вместо INFINITE лимит времени, по истечении которого будет возобновлено исполнение потока - вся байда проводится в цикле, при каждой итерации которого производится обработка сообщений окна;

- Епрст

Ну вот, на вопрос получены очень хорошие ответы. А о многозадачности мы еще обязательно поговорим в одном из выпусков. Главное понять - это не так сложно , как кажется! ;)
____________________________

Q. Можно ли переопределенный обработчик событий сделать подставляемым (inline)? (автор тот же)

На этот вопрос пришел только один ответ, но, на мой взгляд, исчерпывающий, причем тоже от Олега, который умудрился ответить на все заданные в предыдущем выпуске вопросы:

A. Естественно, нельзя. Дело в том, что инлайновые функции не имеют собственного указателя - они похожи на макросы в этом смысле. А диспетчеру обработчиков (если так можно выразиться) надо давать адрес обработчика.

____________________________

Q. Вопрос про ресурсные строки: " ...при нажатии на клавишу "Пропустить" программа идет дальше и вываливается на следующей операции загрузки строки с теми же симптомами и так до тех пор, пока не будут загружены все строки. После этого выполнение программы продолжается в нормальном режиме и все остальное работает как надо (строки все-таки загружаются!). В Release-версии программа пролетает это место без спотыканий."

- Евгений

(Полностью вопрос Евгения см. в предыдущем выпуске, который можно найти по адресу http://subscribe.ru/archive/comp.prog.visualc)

A1. Скорее всего, приложение преобразовано из обычного в MFC и преобразовано некорректно. Нельзя использовать ресурсные функции CString в не-MFC приложении, потому что они требуют специальной инициализации. Другие функции будут работать, а эти нет. Инициализация всех внутренних переменных происходит при инициализации приложения в вызовах AfxWinInit() и прочих служебных функций в AfxWinMain(). Можно посоветовать или сгенерировать приложение заново или использовать код вида:

CString s;
::LoadString(g_hInstance, 12345, s.GetBuffer(256), 256);
s.ReleaseBuffer();

- Дмитрий Дулепов, MCSE

A2. По вопросу о CString::LoadString: Скорее всего, эта функция вызывается до статической инициализации библиотеки MFC. Например, это может произойти в конструкторе объекта, объявленного статическим. Все тот же пресловутый theApp.... :-) Микрософт рекомендует переносить все действия по инициализации в InitInstance, собственно для чего эта ф-ция и предназначена.

- Sergey Emantayev

A3. А по поводу вопроса в рубрике "В поисках истины" - похоже, что зачитка данных идет в конструкторе CMyApp, а не в InitInstance. Предположение насчет некорректной инициализации похоже правильно.

- Oleg Tselobyonok, Applied systems, Ltd.

Огромное всем, кто не поленился написать ответ, особенно Олегу - он у нас сегодня рекордсмен!
Ну вот, кажется на все вопросы получены ответы. Полная идиллия... только вот уже поступили новые вопросы... ;)

/ / / / В ПОИСКАХ ИСТИНЫ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Q. Просто кульно, что ты взялся за эту рассылку, а то, как ты и говорил, в инете нет рассылок по MSVC++. Сам я за ним сижу уже два года, и всё более углубляясь внутрь, возникают всё новые вопросы ;) Но я ещё маленький ;) Хочу задать тебе вопрос: как делать окна нестандартной формы? Например, круг (как у диска Компьютерры - там окно обычное, но с помощью прозрачности виден только круг, так?)

- eFi

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

Должен констатировать, что проблема курсоров не закрыта - пришел еще один вопрос:

Q. Хочу поблагодарить за прекрасную рассылку, надеюсь почерпнуть много интересной и полезной информации. Хочу задать несколько вопросов:
1.Что касается курсоров - как все-таки загружать 256-цветный курсор в приложении? Т.е. проблема в том что в редакторе ресурсов можно сделать либо только черно-белый курсов, либо еще и цветной, но при этом LoadCursor загружет только ч.б. Скорее всего ларчик просто открывается, но все же?
2. Еще вопрос, скорее всего тоже очень популярный. Версия Debug работает без проблем, а при запуске версии Release появляется сообщение о недопустимой операции. Хотелось бы знать в чем проблема и пути ее решения.

- George V. Samodumov

А вот по второму вопросу хочу порекомендовать посмотреть ответ на вопрос Евгения - чуть выше. Как правило, отказ работать debug-версии значит, что все-таки что-то у вас не так, и для вас же будет лучше выяснить, в чем именно проблема. Это может быть связано, например, с инициализацией, с памятью и еще с уймой других вещей.

Q. Спасибо за рассылку - наконец-то свершилось! А то по VB их уже несколько, а по VC++ не было до недавнего времени ни одной. У меня есть вопрос по обработке события WM_KEYUP. Играя с диалогом, обнаружил, что он сам никак не реагирует на нажатия клавы. Как решение, использовал следующий способ: для каждого типа контрола делал свой класс, который реагирует на WM_KEYUP, и в обработчике этого события пересылал сообщение окну диалога. Например, для кнопок создал класс СМуButton, наследуемый от СButton, и в функции CMyButton::OnKeyDown() пересылаю сообщение родительскому окну как GetParent()->SendMessage(...). То же самое для других типов контролов по аналогии.
Но такой способ отдаёт некоторой горбатостью, может быть существует какое-то более элегантное решение?

- Роман Коновалов

Жду ваших писем с ответами!

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Пока все.
Всего вам доброго!

©Алекс Jenter mailto:jenter@mail.ru
 Красноярск, 2000.


No.6

/ *

Программирование на Visual C++

выпуск No.6 от 2/07/2000

* /

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

/ / / / НОВОСТИ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Вышел Service Pack 4

Фирма Microsoft недавно выпустила очередной, четвертый пакет исправлений для Visual Studio 6.0 - Service Pack 4. Он устраняет некоторые ошибки в продуктах VS, а также производит ряд обновлений.
В Visual C++ 6 исправления касаются STL, MFC, CRT, IDE, самого компилятора, отладчика, и многих других частей, т.е. практически всего.
Следует заметить, что среди исправленных ошибок встречаются действительно критические, которые могут помешать работе. Например, частое использование realloc на маленьких блоках памяти вызывало memory access violation (конфликт при попытке доступа к памяти), а DllMain выбрасывала unhandled exception, если в течение DLL_THREAD_DETACH возникала нехватка памяти. MFC AppWizard неправильно связывал данные с программой при использовании OLE DB ODBC provider и Access.
Помимо исправления ошибок, в SP4 включены все предыдущие Service Pack'и, плюс новые версии таких компонентов, как:

  • Microsoft Data Access Objects
  • Microsoft HTML Help
  • Microsoft Data Access Components (MDAC)
  • Microsoft Scripting
  • Microsoft OLE Automation

Также внесены исправления, необходимые для новых версий Office, SQL Server и Windows, включая Internet Explorer.
К сожалению, SP4 не включает самые последние заголовки и библиотеки для Internet Explorer5 или Windows 2000. Их нужно загружать отдельно в виде SDK.

Visual Studio Service Pack 4 занимает около 130 Мб и доступен для свободного скачивания на сайте Microsoft по адресу http://msdn.microsoft.com/vstudio/sp/vs6sp4 (13 файлов, приблизительно по 10 Мб каждый).
Имеется возможность обновить только некоторые продукты, например Visual Basic. Для Visual C++, однако, придется скачивать полную версию. Или подождать, пока она появится на пиратских CD.

/ / / / ОБРАТНАЯ СВЯЗЬ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Из входящей почты

Публикация "WinAPI: Не запутайтесь в типах", вышедшая в предыдущем выпуске (и которая, кстати, будет продолжена в следующем) вызвала некоторый резонанс из-за допущенных там двух неточностей.

Здравствуйте, Алекс!

Встретил в Вашей рассылке "Программирование на Visual C++" за No.5 следующее утверждение:

"Кстати, имейте в виду, что описатель PASCAL и __pascal - это то же самое, что и WINAPI. Но этот описатель является устаревшим, оставлен лишь для совместимости, и Microsoft рекомендует повсеместно использовать вместо него WINAPI."

Это распространенное мнение, которое вызвано переходом Microsoft с __pascal на __stdcall при переходе с Win16 на Win32. При этом Microsoft в MSDN утверждает, что: "Use WINAPI where you previously used PASCAL or __far __pascal." Но это означает всего лишь то, что стандартный способ вызова API функций изменился, а не то, что __pascal эквивалентен __stdcall. Проиллюстрируем это первоисточниками.

MSDN:

"The __stdcall calling convention is used to call Win32 API functions. Argument-passing order Right to left. Argument-passing convention By value, unless a pointer or reference type is passed. Stack-maintenance responsibility Called function pops its own arguments from the stack. "

Borland C++ Builder Help:

"In addition, __pascal declares Pascal-style parameter-passing conventions when applied to a function header (parameters pushed left to right; the called function cleans up the stack)."

Справка от Борландовского продукта в последнем случае выбрана потому, что MSDN вообще умалчивает о том, кто такой этот __pascal, ограничиваясь тем, что он is now obsolete. Из вышеприведенных выдержек мы можем видеть, что __stdcall отличается от __pascal порядком передачи параметров. И просто так подменять один способ другим нельзя. Это легко продемонстрировать, попытавшись вызвать функцию с модификатором __pascal из Visual Basic. Access Violation Вам гарантирован.

- Sergey Shapovalov
Software Security Belarus

Mea culpa. Действительно, иногда не вредно вспоминать, что кроме MSDN существует кое-что еще. Спасибо, Сергей!

___________________

Привет
"Использование соглашения __сdecl вместо __stdcall иногда оправданно, но приводит к увеличению размера >исполняемого модуля из-за того, что имя функции декорируется в этих соглашениях по-разному."

Не понял я этой фразы... __cdecl делает больше код потому что надо стек чистить каждый раз после вызова рутины, а не изза decoration. Names decoration влияет только на представление имён до линкера, в выходной код они не включаются.

Причём, __cdecl - стандартный тип вызова для C/C++ изза возможности использования varargs, в то время как в __stdcall вызове такое невозможно. Как правило, __stdcall юзают в dll (в Win32API в том числе) и для присобачивания чужих lib, собранных с использованием этого типа вызова.

- Ivan Nevraev

В самом деле, декорирование имени функции тут не причем. Благодарю Ивана за помощь. В дальнейшем буду стараться публиковать только перепроверенные данные.

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

___________________

[...] Чтобы рассылка действительно не превращалась в дискуссионную группу, можно дать еще пару ссылочек. Вы уже упомянули news-группы. Можно посоветовать также еще один, правда англоязычный ресурс: http://codeguru.developer.com Это один из (если не самый) крупнейших сайтов для разработчиков на С++ и большей своей частью на Visual C++.
Также там есть discussion board: http://codeguru.developer.com/bbs/wt/wwwthreads.pl где можно задать любой интересующий вопрос (естественно на английском языке). Обычно ответ приходит довольно быстро. Причем это либо исходный код, либо ссылка на что-либо подобное. Короче очень рекомендую.

- Andrew, Gromyko ( gao2000@iname.com )

Андрей, кстати, уже не в первый раз пишет. Ссылки хорошие - сам на CodeGuru захожу очень часто. Так что уверен, многим эта информация придется кстати.
Да, если вы знаете какие-нибудь стоящие внимания ресурсы по VC - присылайте ссылочки!

___________________

Большое спасибо за рассылку! Большой ее положительной спирали! ;) (Bill Gates "The Road Ahead") [ Может, "ей"?... - AJ]

Как насчет новости от Microsoft? Я имею в виду http://msdn.microsoft.com/vstudio/nextgen/technology/csharpintro.asp
С# (pronounced "C sharp").
В связи с этим:
а) не переименовать ли рассылку сразу в VisualC# ? :)
б) не объявить ли конкурс на лучшее объяснение значка # в новом названии; какова связь
# со словом sharp?

Маленькое пожелание напоследок - не забывать, что "VisualC++" != "MFC"; "VisualC++" > "MFC";

Надеюсь еще не раз побеспокоить вас своими письмами.
С уважением,

- Александр Тумель.

Ну, первое пожелание я уже выполнил - появилась рубрика "Новости". Она, конечно, будет появляться не в каждом выпуске, но самые важные события в мире VisualC++ будут освещаться именно там.

О VS NextGen я как раз готовлю материал, даже уже был анонс ("Что дядя Билли нам готовит...") - статья выйдет через выпуск, или , в крайнем случае, через два-три.
А вот насчет произношения "С sharp", гадать, увы, не придется ;) Знак "#" в музыке обозначает диез, т.е. повышение звука на полтона (здесь Microsoft, скорее всего, проводит аналогию с операцией инкрементирования "++"). А "диез" по-английски как раз и будет "sharp" (можете посмотреть в словаре). Все-таки 8 лет муз. школы не прошли для меня даром!;) Так что конкурс отменяется.
Про переименование рассылки в Visual C# подумаем, когда a) прочитаем про этот С# - здесь же;) и б) Microsoft выпустит продукт под таким названием.

А по поводу пожелания - так ведь так оно и есть! Рубрика WinAPI на что? А вот ActiveX и СOM действительно рассылкой не освещаются. Про COM/DCOM кажется, есть отдельная рассылка.

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

На заданные в прошлом выпуске вопросы пришло очень много ответов, так что я, скорее всего, похожие ответы сгруппирую в один, а потом просто укажу, кто такой ответ прислал. Иначе объем выпуска не выдержит ни один почтовый ящик ;) Постараюсь никого не забыть!

Да, и еще. Ради бога, прошу извинить меня тех, кто написал мне и не получил пока ответа. Писем приходит очень много, и я не могу отвечать всем.

На сегодня пока все. Рубрики "WinAPI" и "Вопрос-Ответ" - в следующем выпуске.
Будьте здоровы!

©Алекс Jenter mailto:jenter@mail.ru
 Красноярск, 2000.


No.7

Программирование на Visual C++

выпуск No. 7 от 06/07/2000

Добрый день!

Сегодня я представляю вам обещанное продолжение публикации о типах WinAPI, а также ответы на заданные в 5-ом выпуске вопросы.

/ / / / WINAPI / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

WinAPI: НЕ ЗАПУТАЙТЕСЬ В ТИПАХ

(Продолжение. Начало см. выпуск No.5)

Очень часто вами будет использоваться тип HANDLE - дескриптор, предназначенный для описания различных объектов. На самом деле этот тип представляет собой ни что иное, как указатель на void, т.е. как бы на любой тип.

Объекты Windows обычно представлены своими дескрипторами. Например, HWND - дескриптор окна. Что он из себя представляет? Давайте посмотрим:
В файле windef.h можно обнаружить такую строчку:

DECLARE_HANDLE (HWND);
Эта строка при определенной опции STRICT разворачивается в

struct HWND__ {int unused;};
typedef struct HWND__ *HWND

То есть HWND есть указатель на структуру HWND__. Если же опция STRICT не определена, то HWND везде заменяется на HANDLE.
Идентификатор STRICT указывает на необходимость проводить более строгую проверку типов. Как вы уже убедились, без этой опции все HWND, а также описатели других объектов Windows - HPEN, HBITMAP, HFONT, HMENU, HDC и др. будут фактически представлять собой один тип - HANDLE. Если же вы включите определение STRICT, тогда они будут трактоваться как разные типы (благодаря макросу DECLARE_HANDLE), и при их несоответствии компилятор будет выдавать сообщение об ошибке. Использование STRICT рекомендуется для того, чтобы было легче находить возможные ошибки в программе.

В заключение давайте рассмотрим очень часто используемый тип COLORREF. По сути это unsigned long. Этот тип представляет возможность задать цвет набором его RED, GREEN и BLUE составляющих, для этого используйте макрос RGB:

 COLORREF color=RGB(0,255,255);

Результат этого выражения - длинное целое число, самый младший байт которого содержит интенсивность красного, второй - зеленого и третий байт - синего. В этом случае color будет содержать голубой цвет. Сам макрос RGB(r,g,b) при обработке препроцессором расширяется до ((COLORREF)((BYTE)(r) | ((WORD)(g) <<8)) | (((DWORD)(BYTE)(b))<<16))).

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

Q. ... Как делать окна нестандартной формы? Например, круг (как у диска Компьютерры - там окно обычное, но с помощью прозрачности виден только круг, так?)

- eFi

A. На этот вопрос пришло довольно много фактически одинаковых ответов, и я сейчас постараюсь объяснить самую суть.
В Windows существует понятие регионов - областей. Каждое окно имеет свою область, которая по умолчанию создается прямоугольной. В WinAPI (и в классе CWnd) существует функция SetWindowRgn( ), которая позволяет задать форму этой области. То есть сначала вы создаете область, потом устанавливаете его как форму для окна (это можно сделать, например, в OnInitDialog( ) ). Создать область можно с помощью функций Create...Rgn( ). Например, чтобы сделать круглое окно, можно воспользоваться CreateEllipticRgn( ). Подробно параметры я описывать не буду - смотрите пример. Замечу только, что регионы можно создавать сложные, составленные из нескольких примитивов. Они образуются путем комбинирования областей ( CombineRgn( ) ).

Пример (прислал Sergey Melnikov):

 CRect Rect;
 GetWindowRect(&Rect);
 HRGN hRgn = CreateEllipticRgn(0, 0, Rect.Width(), Rect.Height());
 SetWindowRgn(hRgn, TRUE);

А если добавить такой код, получим окно с "прорезью" в виде эллипса:

 HRGN hRgn1 = CreateRectRgn(0, 0, Rect.Width(), Rect.Height());
 HRGN hRgn2 = CreateEllipticRgn(0, 0, Rect.Width(), Rect.Height());
 HRGN hRgn3 = CreateRectRgn(0, 0, Rect.Width(), Rect.Height());
 CombineRgn(hRgn3, hRgn1, hRgn2, RGN_DIFF);
 SetWindowRgn(hRgn3, TRUE);

Ответ на этот вопрос прислали (в порядке получения): Андрей Колчанов, Ренат Васиков, Ilgar Mashayev, Sergey Skornyakov, LiMar, Sergey Melnikov, Igor Kurilov, Michael Stepanenkov.

_____________________

Q. ..Как загружать 256-цветный курсор в приложении? Т.е. проблема в том что в редакторе ресурсов можно сделать либо только черно-белый курсор, либо еще и цветной, но при этом LoadCursor загружет только ч.б...

- George V. Samodumov

A. Ответ на этот вопрос часто сводится к рекомендации воспользоваться LoadImage( ) вместо LoadCursor( ). Вот самый полный и интересный ответ из присланных:

Дело в том, что файл курсора имеет схожий формат с файлом иконки, т.е. в одном файле могут находиться несколько изображений разных форматов, например: 16х16х16, 32х32х256 и т.д. При добавлении нового курсора редактор ресурсов VC автоматически создает курсор формата 32х32х2, который вероятно и грузится первым даже если добавлены еще несколько изображений. Поэтому нужно сделать так, чтобы курсор содержал только одно изображение. В редакторе ресурсов выполняем Insert|Cursor, потом открываем его для редактирования и в появившемся меню Image выбираем "New Device Image", а там "Custom" и задаем параметры изображения, например 48x48x256. Редактируем курсор, а потом переключаемся на монохромное изображение и удаляем его: "Image|Open Device Image -> Monochrome32x32", "Image|Delete Device Image". Теперь мы избавились от монохромного изображения и можем грузить курсор функциями: LoadCursor(), LoadCursorFromFile(), LoadImage():

BOOL CSampleDlg::OnInitDialog()
{
CDialog::OnInitDialog();

::SetClassLong(m_hWnd,GCL_HCURSOR,
(LONG)(HCURSOR)AfxGetApp()->LoadCursor(IDC_CURSOR1));

// Грузим анимационный курсор
(LONG)(HCURSOR)::LoadCursorFromFile("Appstart.ani"));

return TRUE;
}

- Alex Hin

Ответ прислали (в порядке получения): Azanov Max, Dmitri A. Doulepov, Alex Hin, Igor Kurilov.

_____________________

По многочисленным просьбам уменьшить размер выпуска, одновременно выпуская его чаще, оставшиеся два вопроса будут рассмотрены в следующем выпуске рассылки, который выйдет послезавтра (в субботу).

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Предлагаю подписаться на дружественную рассылку:

Visual Basic - Трюки и Хитрости, советы и ответы на вопросы
Рассылки Subscribe.Ru
COM/DCOM - вокруг да около

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Всего хорошего!

А рхив рассылки: http://subscribe.ru/archive/comp.prog.visualc
Статистика рассылки: http://subscribe.ru/stat/comp.prog.visualc

©Алекс Jenter mailto:jenter@mail.ru
 Красноярск, 2000.


No.8

Программирование на Visual C++

выпуск No. 8 от 08/07/2000

Здравствуйте!

/ / / / НОВОСТИ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Что дядя Билли нам готовит, или
 Visual Studio Next Generation

Многие из вас наверняка гадали - а что дальше? Какой он будет, новый Visual C++? Какие планы на этот счет у Microsoft? Ну что ж, сейчас завеса тайны более-менее приоткрылась, и можно уже о чем-то говорить.
Прежде всего следует заметить, что Visual Studio 7.0 будет больше ориентирована на разработку Web-приложений. Дело в том, что Microsoft недавно представила свою новую платформу .NET (читается "dot net"), предназначенную для еще большей интеграции интернета в операционную систему. Уже следующая ОС Windows, пока известная под кодовым названием Whistler и ожидаемая только к 2001 году, будет основана на этой платформе. Новая версия Visual Studio должна значительно упростить разработку программ для интернета.

На конференции VC-разработчиков фирмой Microsoft были заявлены основные планируемые изменения и улучшения, которые коснутся Visual C++. Итак, стало известно следующее:
1) Планируется ввести унифицированную среду разработчика (IDE) - теперь VC и VB будут одной средой. Хорошо это или плохо, покажет только время. Я не берусь сейчас давать оценку этому нововведению, потому что очень много будет зависеть от того, как именно это будет реализовано. Из плюсов такого подхода следует отметить то, что теперь независимо от того, на каком языке вы собираетесь программировать, вам нужно освоить только одну среду, а если в будущем вам придется воспользоваться другим языком, изучать другую IDE будет уже не нужно. Тем более это актуально, если вы планируете использовать оба языка.
2) Следующий релиз VS будет включать в себя технологию, называемую "ATL-сервер". Эта технология предложит набор классов-расширений для Active Template Library (ATL) и будет обеспечивать доступ ко всем функциям Internet Information Server (IIS). Microsoft полагает, что это значительно упростит и ускорит процесс создания масштабируемых Web-приложений.
3) Расширение для языка - "Attributed programming" - (Я бы перевел как "описательное программирование", программирование за счет свойств). Оно призвано уменьшить объем кода, который программисты должны писать для создания COM+ компонентов. Свойства инкапсулируют доменные понятия (такие, как Data, COM, Web Services) в простые объявления, которые вставляются прямо в исходный код. Эти объявления предоставляют компилятору Visual C++ всю необходимую контекстную информацию, что раньше требовало сотни строк кода.

Следует также отметить, что все новые продукты Microsoft, спроектированные для платформы .NET, будут иметь расширенную поддержку XML.

И, наконец, самое интересное. Microsoft анонсирует новый язык программирования, который будет называться "C#" (читается как "C sharp". Мы, кстати, уже обсуждали связь знака "#" со словом sharp в выпуске No. 6 ).

С# - это новый объектно-ориентированный язык программирования, который позволит разработчикам быстро создавать широкий диапазон приложений для новой платформы .NET.
Новый язык будет конкурировать скорее с Java, чем с C++. Естественно, Microsoft совершенно не устраивает Java, прежде всего потому, что Java - это детище Sun Microsystems. Еще одна причина создать альтернативу Java - патологически малая производительность последнего, большей частью обусловленная его мультиплатформенностью. Как и Java, новый язык будет основан на C++. В C# будет встроена поддержка COM и XML, причем последний будет стандартным форматом структурированных данных для посылки через интернет.
В C# любой объект - это COM-объект. Разработчикам больше не придется явно реализовывать IUnknown и другие COM-интерфейсы. Программы на C# также смогут легко использовать любой COM-объект, независимо от того, на каком языке он был написан.
Новый язык спроектирован таким образом, чтобы исключить частые ошибки программистов. Например, "сборка мусора" позволит разрешить главную проблему С++ - неправильно используемые указатели; переменные в C# будут инициализироваться автоматически, а cам язык будет обладать повышенной типовой безопасностью.
Описание языка в формате MS Word (на английском языке, естественно) все интересующиеся могут скачать отсюда.

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

Прошу прощения у Александра Панченко, который также прислал ответ на вопрос о 256-цветных курсорах, но не был мной упомянут в прошлом выпуске, и у Ивана Невраева (Ivan Nevraev), который также написал ответ на вопрос о нестандартной форме окна.

Сейчас рассмотрим ответы на два оставшихся вопроса из выпуска No.5:

Q. Версия Debug работает без проблем, а при запуске версии Release появляется сообщение о недопустимой операции. Хотелось бы знать в чем проблема и пути ее решения.

- George V. Samodumov

A1. VC++ порой глючит и не делает перекомпиляцию файлов, либо не линкует исправленные файлы при incremental link-е. Если выполнить build-all то иногда можно обнаружить что-нибудь вроде синтаксической ошибки, хотя до этого
компиляция проходила без проблем.

- Igor Kurilov

A2. Это явно что-то с указателями. Причём как при их использовании, так и при повторном освобождении. В debug-версии MFC помечают память, на которую указывают освобождаемые указатели символами 0xСD. Соответственно, в отладчике Вы сразу поймёте, что что-то не так, если увидите такое значение. При этом может невозникать исключений. Если работать под WinNT они возникнут почти наверняка, а вот Win9x ведёт себя в этом отношении проще: никаких исключений не возбуждается. При использовании release-версии объекты инициализируются по-умолчанию в NULL. Попытка использования такого указателя вызовет исключение как в WinNT, так и в Win9x. Всё это проверено не раз на практике...

- Dmitri A. Doulepov, MCSE

A3. [...] При проблеме с Release-версией рекомендую следующее:
1. Откомпилировать программу с оптимизацией, но и с подключенной отладочной информацией. Для этого она должна генерироваться компилятором и подключаться линковщиком. Дальше запускаем под отладчиком и смотрим ошибки.
2. После локализации ошибки проанализировать соответствие ассемблерного кода (Ctrl+F7 в MSVC) исходному коду.
Типичные причины подобных ошибок:
1. Оптимизатор дооптимизировался до маразма
2. В отладочном режиме свободные области памяти заполняются определенным значением. В релизе этого не происходит. Выплывают огрехи с отсутствием инициализации переменных
3. Время выполнения участков кода становится другим. Могут всплыть ошибки, связанные с некачественной синхронизацией различных ниток и т.п.[...]

- Nikita Zeemin

A4. Могу сказать только одно - ЕЩЕ РАЗ проверьте свой код. У меня такие ситуации были на заре моей практики. Это было ужасно... Часы проведенные в Debuge - куча MessageBox в Release - и ничего - ошибка не исчезала. А было на самом деле вот что. Под Debug компайлер напихивает кучу разных вещей которых нет в Release версии - как например обнуление указателей, проверку на выделение памяти и т.д. Я приведу здесь один глюк который реально у меня встретился и привел к вышеупомянутым последствиям. Был какой-то класс и какая структура данных
    struct MyStruct  { ...  };
    class MyClass
    {
        MyStruct* pStruct;
        ...
    }
И мне надо было в конструкторе класса выделить память под pStruct - но я этого НЕ СДЕЛАЛ. И было вот что под Debug обращение типа pStruct[0] не вызывало никаких осложнений - а под Release вылетало тут же. Поэтому следите за УКАЗАТЕЛЯМИ. Это самая кульная вещь в С/С++ но и самая геморройная (может быть грубовато - но это так)

- Alexey Merkulov

A5. По поводу скрытых ошибок в программах, не ленитесь в критических местах
    использовать TRACE0, а перед использованием указателей проверяйте содержимое

HRESULT AnyMethod(MyClass* pPointer)
{
if(pPointer != NULL)
{
// и только здесь начинайте с ним работать!
}
else
{
TRACE0("Получен пустой указатель");
return S_FALSE;
}
}

- Alexei A. Zanine, System Engineer

_____________________

Q. У меня есть вопрос по обработке события WM_KEYUP. Играя с диалогом, обнаружил, что он сам никак не реагирует на нажатия клавы. Как решение, использовал следующий способ: для каждого типа контрола делал свой класс, который реагирует на WM_KEYUP, и в обработчике этого события пересылал сообщение окну диалога. [...] Но такой способ отдаeт некоторой горбатостью, может быть существует какое-то более элегантное решение?

- Роман Коновалов

A. На этот вопрос пришли похожие ответы, суть которых сводится к совету перекрыть функцию PreTraslateMessage( ) и все нажатия обрабатывать там. Такие ответы прислали Igor Sorokin , Дмитрий Елюсеев и Alex Hin.

Dmitri A. Doulepov советует также обратить внимание на функцию IsDialogMessage( ).

Я поясню - эта функция вызывается из CWnd::PreTranslateMessage( ) для того, чтобы определить, предназначено ли сообщение для диалога. Если да, то она обрабатывает это сообщение, проверяет клавиатурные сообщения и конвертирует их в команды диалогового окна (например, TAB преобразуется в команду перехода к следующему элементу управления.)

Пример:

BOOL CAboutDlg::PreTranslateMessage(MSG* pMsg)
{
// TODO: Add your specialized code here and/or call the base class
if (pMsg->message==WM_KEYUP && pMsg->wParam== VK_DOWN)
{
MessageBox("DOWN KEY WAS RELEASED!");

return TRUE; // уберите это,
    если хотите, чтобы
// сообщение еще обработалось и
    стандартным образом
}

// вызываем стандартную
    обработку, оттуда будет
// вызвана PreTranslateInput(), откуда, в свою
// очередь, вызывается IsDialogMessage()
return CDialog::PreTranslateMessage(pMsg);
}

/ / / В ПОИСКАХ ИСТИНЫ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Я решил, что будет лучше публиковать по одному вопросу в выпуске. Так и размер выпусков будет меньше ( повторюсь, меня не раз укоряли за то, что выпуски получаются слишком "тяжелые"), да и проще ссылаться на вопросы - по номеру выпуска.

Вопрос сегодняшнего выпуска:

Q. Нужно изменить шрифт у одного элемента типа CStatic. Делаю это функцией SetFont(CFont font). Фонт меняется у элемента ... и у всего окна :(. Включая кнопки и другие элементы типа static. Мне его надо было толстым сделать, так у меня такие кнопки стали -загляденье:)) Кто-нибудь знает в чем дело и как решить ?

- LiMar

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Предлагаю подписаться на дружественную рассылку:

COM/DCOM - вокруг да около

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Все на сегодня. Пока!

©Алекс Jenter mailto:jenter@mail.ru
 Красноярск, 2000.


No.9

Программирование на Visual C++

выпуск No.9 от 11/07/2000

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

/ / / / ОБРАТНАЯ СВЯЗЬ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Из входящей почты

Мы с вами уже разобрали ответы на вопрос о том, почему в Debug-версии все иногда работает нормально, а в Release появляются большие проблемы (этот вопрос был задан в выпуске No.5). Уже после того, как вышел выпуск с ответами на этот вопрос, пришли еще несколько писем на эту тему. Большинство сожалеет о том, что такой "элементарный" нюанс - а именно, чреватость использования макроса ASSERT, - остался вне обсуждения.
Для тех, кто не понял, в чем здесь дело: макрос ASSERT(<условие>), в отличие от сходного макроса VERIFY(<усл> ), работает только в Debug-версии, а в Release-версии этот макрос просто заменяется пустой строкой, следовательно условие, которое указывается в скобках, не проверяется. Таким образом, если ваша программа нашпигована такими вот макросами, и вы компилируете ее как Release, проверка всех условий совершенно незаметно для вас исчезает.

А теперь у меня вопрос к авторам таких ответов : Каким образом в Debug-версии все может быть нормально, если исчезновение ASSERT'ов оказалось критичным для работы Release-версии? (Хотя, если честно, один такой способ существует, и именно его, скорее всего. имели ввиду авторы писем. Но я просто никогда еще не встречал таких оригиналов, которые в условие макроса ASSERT умудрятся впихнуть что-нибудь помимо самого условия, выделение памяти или инициализацию объекта, например. Никогда так не делайте! Впрочем, уверен, что большинство до такого все-таки не додумалось ;)
Итак, выходит в Debug-версии программа должна была вылетать на "Assertion failed", а это вряд ли можно назвать "нормальным выполнением". Напоминаю, в самом вопросе утверждалось, что в Debug программа работает без проблем.

Вообще, макрос ASSERT предназначен как раз для того, чтобы именно Debug-версия и не работала , если у вас что-то в программе не в порядке! Таким образом, программист сможет сразу понять, что и где у него не так (это, конечно, в идеале ;).

Но замечу, что сам по себе нюанс этот достаточно интересный. Итак, люди - обратите внимание на макросы ASSERT и VERIFY! Напоминаю: VERIFY, в отличие от ASSERT, сохраняется и в Release-версии, хотя в последнем случае он не прерывает программу даже если условие не выполняется.
Читателей, поднявшим этот вопрос, благодарю, а это: Alexander Dymerets, Alexey "Locky" B.R. и Serge Zakharchuk.

В отличие от большинства, Olga Zamkovaya предложила другой способ выяснить, в чем дело:

...К вопросу о недопустимой операции в Release версии программы из выпуска #5: в числе полезных советов "проверьте свой код", "build all может помочь" и т.п. не было предложено воспользоваться опцией компилятора /GZ (catch Release-build errors in Debug build), что, мне кажется, может быть полезно в данной ситуации.)

- Olga Zamkovaya

Что ж, думаю, и это кому-то поможет - ловить Release-ошибки в Debug. По крайней мере можно будет обнаруживать ошибки на стадии, которая как раз предназначена для отлова ошибок;) Thank you, Olga.

___________________

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

Здравствуйте, Алекс!

Решил попробовать свои силы во внесении посильного вклада в понимание не самых понятных вещей, которые касаются каким-либо образом MS VCPP.
Итак, в выпуске №5 промелькнул вопрос об обработке клавиш в диалоге. Я в свое время столкнулся с точно таким же вопросом и даже собирался его решать способом, которым решил автор вопроса, но меня не хватило: я
ленивый. Я нашел очень полезную вещь: использование акселераторов (горячих клавиш) - accelerators - в диалогах. Пользуюсь этим способом регулярно и до сих пор. Идея, в принципе, та же: перегрузить PreTranslateMessage.

Код для этой функции:

BOOL CSomeDialog::PreTranslateMessage(MSG* pMsg)
{
 if (pMsg->message >= WM_KEYFIRST && pMsg->message <= WM_KEYLAST)
  if (m_hAccel)
   if (::TranslateAccelerator(m_hWnd, m_hAccel, pMsg))
    return TRUE;

 return CDialog::PreTranslateMessage(pMsg);
}

Здесь m_hAccel - переменная-член класса CSomeDialog типа HACCEL, инициализированная в OnInitDialog таким, например, способом: m_hAccel = ::LoadAccelerators(AfxGetInstanceHandle(), MAKEINTRESOURCE(m_lpszTemplateName)); Если ее инициализировать таким образом, то будет произведена попытка
найти ресурс акселератора с тем же ID, что и ID диалога (например, IDD_SOMEDIALOG), в котором можно прописать какие только душа пожелает клавиши и их комбинации. Если же ресурс найден не будет, то ничего страшного не произойдет.
Обрабатывать команды от акселератора можно стандартным способом - ON_COMMAND в MESSAGE_MAP'е. Я их прописываю руками, без ClassWizard'а. Да, кстати, можно запросто лепить в таблицу акселератора IDшки кнопок (push buttons). Хэндлер для обработки кнопки, объявленный с помощью ON_BN_CLICKED, будет вызван автоматически (это связано с тем, что ON_COMMAND и ON_BN_CLICKED на самом деле - одно и то же).
[. . . ]
Спасибо, что дочитали даже до этого места, надеюсь, содержанием не разочаровал. Ваша рассылка уже rules, а она (я надеюсь) только начинает раскачиваться.

Спасибо за вашу работу и за ее результат.
--
Пишите письма...
(адрес может быть опубликован, но не продан спаммерам :)

- Чепкий Николай ( mailto:alterego@a-teleport.com)

Адрес я опубликовал, но спаммерам не продавал - так что моя совесть на этот счет чиста. ;) Если это сделает кто-нибудь из читателей - это будет на его, а не моей, совести.

Вопрос этот обсуждался в прошлом выпуске. Преимущество способа, предложенного Николаем, заключается в автоматизации обработки нажатий клавиш. Так что вместо неуклюжего switch'a в случае большого количества клавиш мы получаем удобный списочек - и минимум кода.

___________________

Один из читателей прислал интересный совет, предлагаю его вашему вниманию:

Привет!
Хочу обратить внимание на то, что изменение формы окон при помощи SetWindowRgn() не всегда правильно работает в старых версиях Windows -- в частности, такая ситуация наблюдалась под Windows 95 (PLUS) не OSR 2.

Зато совершенно точно это работает под '98, NT, 2000.
-------
Хочу предложить полезную уловку, позволяющую при использовании MFC-шаблонов документов управлять MDI-окнами из приложения. Этот трюк можно использовать при отображении разных категорий данных в различных окнах. При этом можно, в частности, автоматически переключать активные MDI-окна при обновлении данных в них.

Представьте библиотеку (класс), следующего вида:

class TReg
{
 public:
   static CMapStringToPtr map;

   static BOOL RegisterTemplate( CString strName, CDocTemplate * ptr );
   static BOOL HasOpenViews( CString strName );
   static BOOL PostForAllViews( CString strName, UINT msg, WPARAM w, LPARAM p );
   static BOOL SendForAllViews( CString strName, UINT msg, WPARAM w, LPARAM p );

   static CDocTemplate * GetTemplate( CString strName );
. . .
};

Зачем все элементы статические -- легко понять, ведь у нас только один MDI-фрейм.

Далее, в методе WinApp::InitInstance() при порождении шаблонов документов вместо (или вместе с) AddDocTemplate( CDocTemplate * ) записываем TReg::RegisterTemplate( "MyName", CDocTemplate * );

Здесь мы просто добавляем указатели шаблонов в словарь map.

С помощью метода GetTemplate() мы можем извлечь указатель на шаблон из словаря по имени. Используя этот указатель, мы можем:
- открыть новое окно при помощи DocTemplate::OpenDocumentFile();
- закрыть все окна, относящиеся к данному шаблону;

- отправить сообщение всем окнам данного шаблона:

for ( POSITION pos= pTempl->GetFirstDocPosition (); pos != NULL )
{
  CDocument * pDoc= pTempl->GetNextDoc (pos);

  if( msg == NULL )
    pDoc->UpdateAllViews( NULL );
  else
    for ( POSITION p1= pDoc->GetFirstViewPosition (); p1 != NULL; )
    {
      CView * pView= pDoc->GetNextView (p1);
      pView->PostMessage (msg, w, l);
    }
}

- проверить, имеются ли открытые окна, относящиеся к данному шаблону:

for ( POSITION pos= pTempl->GetFirstDocPosition (); pos != NULL )
{
 CDocument * pDoc= pTempl->GetNextDoc (pos);

 for ( POSITION p1= pDoc->GetFirstViewPosition (); p1 != NULL; )
 {
  CView * pView= pDoc->GetNextView (p1);

  if( pView != NULL ) return TRUE;
 }
}
return FALSE;

и т.д.

Активизация (всплывание наверх) MDI-окна в программе проще всего реализуется добавлением примерно такого метода класса CView:

void CMyView::DoActivate( )
{
  CMDIChildWnd * pFrm = (CMDIChildWnd *)(GetParent( ));

  if( pFrm != NULL && IsWindow( pFrm->m_hWnd ) )
    pFrm->MDIActivate( );
}

- Victor Yakovlev

Да, это может быть полезно, особенно для тех, кто сталкивался (или кому еще только предстоит столкнуться) с разработкой сложных MDI-приложений - они знают, как трудно добиться правильной совместной работы всех дочерних окон.

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

Q. Нужно изменить шрифт у одного элемента типа CStatic. Делаю это функцией SetFont(CFont font). Фонт меняется у элемента ... и у всего окна :(. Включая кнопки и другие элементы типа static. Мне его надо было толстым сделать, так у меня такие кнопки стали -загляденье:)) Кто-нибудь знает в чем дело и как решить ?

- LiMar

A. Присланные ответы на этот вопрос сводились к двум следующим:
1) сделать класс-наследник от CStatic и перекрыть функцию прорисовки - OnPaint( );
2) вызывать метод SetFont( ) именно объекта CStatic (или указателя на этот контрол), а не всего диалога.

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

Итак, второй ответ был больше всего похож на искомую истину. Но "похож" - это еще не значит "есть", так что я решил проверить. Сделал простое SDI-приложение, и попробовал в окне About у одной из надписей поменять шрифт.
Как же я был рад, когда он в самом деле изменился!!!

...Правда, на совершенно не тот, который я хотел. Да и размерчик прежний остался... Это было весело - в любом случае он ставил шрифт System, хотя (у меня много шрифтов!) я прописывал разные. Никакого результата. Способ No.2 у меня не работал. Либо он был неправильный, либо, как впоследствии оказалось, правильный не до конца.
Через некоторое время мне это надоело, и я решил, что раз уж не оказалось пророков среди читателей, пророком придется стать самому (это метафора;)

Самое обидное то, что ответ даже не пришлось искать ! Он лежал на самом видном месте в MSDN. Я ввел "SetFont" в строке поиска и мгновенно обнаружил интереснейшую статью с говорящим само за себя названием - "Correct Use of the SetFont( ) Function in MFC".
Суть статьи сводилась к следующему:
Обычно в SetFont передают указатель на шрифт - объект CFont. Так вот, обязательно нужно проследить, чтобы этот объект не уничтожился раньше, чем тот контрол, для которого он создается!

Итак, как было у меня раньше (или "способ No.2"):

BOOL CAboutDlg::OnInitDialog()
{

  CDialog::OnInitDialog();

  CFont times;

  times.CreatePointFont (100,"Times New Roman");
  m_Static.SetFont(&times);

  times.DeleteObject();

  return TRUE;
}

m_Static - переменная, представляющая соответствующий Static-контрол. Вместо нее можно воспользоваться указателем, возвращаемым ф-цией GetDlgItem( ). Как вы видите, объект CFont уничтожается сразу же после вызова SetFont( ).

А вот как надо было сделать:

class CAboutDlg : public CDialog
{
 ....
 private:
  CStatic m_Static;
  CFont m_fntTahoma;  // добавляем шрифт в диалог
}

BOOL CAboutDlg::OnInitDialog()
{

  CDialog::OnInitDialog();

  m_fntTahoma.CreatePointFont (100,"Tahoma");
  m_Static.SetFont(&m_fntTahoma);

  return TRUE;
}

BOOL CAboutDlg::DestroyWindow()
{
  m_fntTahoma.DeleteObject();

  return CDialog::DestroyWindow();
}

Здесь все работает как надо. Вскоре, когда надоела Tahoma, я уже наслаждался отлично выглядевшей готической надписью. (Кстати, тут возникает еще вопрос - получается, чтобы нужный шрифт был всегда доступен, нужно распространять его вместе с приложением? Конечно, это не относится к стандартным Windows-шрифтам, типа Arial, Times, Tahoma или Courier. Лучше все-таки обходиться ими, когда возможно).
Тех, кто хочет получить больший контроль над шрифтом - сделать его жирным, курсивом и т.д. , отправляю прямиком к той же статье, да еще к функции СFont::CreateFontIndirect( ).

Я прошу прощения, что, возможно, слишком подробно расписал ответ на этот вопрос (хотя не исключаю, что кому-то это было интересно прочитать). Я преследовал еще одну цель - сказать всем: "Люди, учитесь пользоваться MSDN! На многие ваши вопросы там уже отвечено!"

Ответ на этот вопрос прислали: Николай Чепкий , Igor Sorokin, Alexander Dymerets, Pavel Vasev.

/ / / / В ПОИСКАХ ИСТИНЫ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Q.Как получить доступ к ресурсам DLL в самой DLL? Задача сводилась к следующему - нужно было сделать диалоговое окно в функции, которая находилась в DLL.

__declspec(dllexport) int MyDllFunction()
{
CDialog dlg ;
int ret = dlg.DoModal() ;
return ret ;
}

DLL имела ресурс Dialog для этого диалогового окна, но работать напрочь отказывалась - этот ресурс не обнаруживался и окно не создавалось. DLL собиралась как со статически линкуемой библиотекой MFC, так и с динамически линкуемой библиотекой MFC.

- Igor Sorokin

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

За сим откланиваюсь.
Будьте здоровы!

©Алекс Jenter mailto:jenter@mail.ru
Красноярск, 2000.


No.10

Программирование на Visual C++

выпуск No.10 от 18/07/2000

Здравствуйте!

Прошу прощения за небольшую задержку этого выпуска.

Между прочим, со дня создания рассылки уже прошел целый месяц. За это время на нее подписалось более 5000 человек, причем чуть меньше 4000 из них - на HTML версию.

/ / / / WEBзор / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Недавно мне пришло письмо от одного человека с предложением рассказать в рассылке об их сайте, часть которого посвящена как раз программированию на Visual C++. Тогда у меня появилась идея сделать эту рубрику, где я мог бы рассказывать об интересных ресурсах по теме рассылки, которые можно обнаружить на просторах интернета. Я уже, правда, рассказывал про CodeGuru - но это не было вынесено в отдельную рубрику... да и те, кто не очень дружит с английским, могли задаться вопросом, а нет ли в Сети чего-нибудь интересного по VC на русском языке?

Итак, сегодня, по специальному предложению одного из авторов, мы с вами рассмотрим сайт "ПЕРВЫЕ ШАГИ" (http://www.mjk.msk.ru/~dron/ )

Прежде всего, стоит отметить широкий диапазон тем, охватываемых этим сайтом - это MFC, WinAPI, OpenGL, ActiveX, а также VBA, SQL, HTML, CGI, Perl, форматы файлов, алгоритмы и многое другое.

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

Все описанное мной далее относится к разделу "Visual C++".

Есть материал как для начинающих, так и для более продвинутых программистов. Раздел, посвященный MFC, содержит более 200 "шагов". Темы шагов в большинстве своем подобраны интересные и нужные, а для новичков, не имеющих возможности свободно читать MSDN, вообще незаменимые.

Из недостатков хочу отметить некоторую сухость и краткость изложения (хотя последняя, конечно, сестра таланта), иногда все выливается в "нажми туда-то, напиши то-то". Также мне не совсем по вкусу местами чересчур неофициальный и немного непоследовательный стиль автора, но это кому как, наверное.

ОЧЕНЬ СИЛЬНО портит впечатление обилие опечаток и орфографических ошибок (не говоря уже о пунктуационных) ... во многих из "шагов" я мог насчитать не меньше двух. Обидно! В крайнем случае - вроде спел-чекеры уже не редкость...

Не знаю, как сейчас, но раньше, помню, сайт не позволял себя скачивать целиком программами типа Teleport Pro. Мне это казалось совершенно неоправданным и ненужным ограничением - в конце концов, у нас ведь не Америка, где неограниченный доступ стоит 15-20 долларов в месяц. В гостевой книге 90% записей были посвящены этому вопиющему безобразию... Авторам сайта, наверно, это надоело, и они гостевую книгу убрали совсем... ;)

Хотя выполнять свое предназначение сайту все это, конечно, не мешает. Да и критиковать легко...
Зайдите, посмотрите, - скорее всего, найдете что-нибудь интересное и для себя.

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

Q. Как получить доступ к ресурсам DLL в самой DLL? Задача сводилась к следующему - нужно было сделать диалоговое окно в функции, которая находилась в DLL

__declspec(dllexport) int MyDllFunction()
{
CDialog dlg ;
int ret = dlg.DoModal() ;
return ret ;
}

DLL имела ресурс Dialog для этого диалогового окна, но работать напрочь отказывалась - этот ресурс не обнаруживался и окно не создавалось. DLL собиралась как со статически линкуемой библиотекой MFC, так и с динамически линкуемой библиотекой MFC.

- Igor Sorokin

A1. В вопросе приводился пример функции, с помощью которого предполагалось вызвать диалог. В MSDN я нашe:л статью TN058, рассказывающую о том, как реализовано управление модулями в MFC.
Для того, чтобы получить доступ к любому объекту MFC из экспортируемой функции, необходимо в самом начале функции поставить AFX_MANAGE_STATE( AfxGetStaticModuleState() ) ;
Таким образом, будут корректно реализована связь дескрипторов (HANDLE) с объектами MFC и , в частности, ресурсы, хранимые в DLL будут корректно задействованы:

__declspec(dllexport)
int MyDllFunction()
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
CDialog dlg(IDD_TESTDLG);
return dlg.DoModal();
}

- Алексей Селезнев

A2. Вообще говоря, доступ к ресурсам DLL из самой DLL получать не надо - он и так дан. Но пример, указанный в вопросе, по-моему, чуть-чуть не правильный, и работать никогда не будет, потому как "CDialog dlg" не проинициализирован как следует. Пусть в DLL-проекте создан ресурс-диалог, с идентификатором (например) IDD_RTNDIALOG. Для того, показать этот ресурс-диалог (в модальном режиме), надо выполнить:
CDialog dlg( IDD_RTNDIALOG);
dlg.DoModal();
Здесь конструктору объекта dlg передаe:м ID ресурса - нашего диалога. Можно еще указать родительское окно. Чтобы показывать этот диалог немодально, следует использовать Create/[ShowWindow/WS_VISIBLE].

Однако, если мы хотим, чтобы диалог содержал всякие контролы, помимо OK и Cancel, то нужно на основе ресурса-диалога создать класс-наследник CDialog'a. (в MFC - например с даблкликнув на форму шаблона). Пусть мы создали класс с именем CRtnDlg. Он и будет реализовывать всякие обработчики контролов.
Показать модально проще простого:
CRtnDlg dlg;
dlg.DoModal();
Немодально - use ShowWindow(...);

Кстати, в описании CRtnDlg.h нужно не забыть вставить #include "resource.h" - а то компилятор тоже ресурса не увидит :)))
Кстати, на счет примера - Игоря я обидеть не хотел, может он его так, для пояснения сути написал.

3. Вопрос:

Недавно я писал курсовую - рисует всякие дифуры. А каждое конкретное диф.ур-ие реализуется в отдельной DLL'ке (по типу plugins). А т.к. мои DLL с дифурами имели единый интерфейс(не COM), для них я сделал шаблон. А потом случилось страшное - DLL-проекты, созданные по шаблону, не компилировались, с дурацкими ошибками. Короче, через сутки я выяснил, что
почему-то в проекте, созданном по шаблону, директива компилятора _AFXEXT заменяется на _USRDLL (в результате мой DLL плавно превращается из MFC extension в Regular DLL).Шаблон создавал по существующему проекту. Ни в исходном проекте, ни в шаблоне ничего в опциях не путал. Приходилось потом вручную каждый раз изменять директивы. А в чем же дело? Может знает кто?
4. Алекс, и ещe: - на счет ответов на вопрос в N8. Я там между прочим указывал, что объект CFont нужно сделать членом класса окна, т.к. передаe:тся указатель.

- Pavel Vasev

Насчет пункта 4 Павел совершенно прав, я просто не обратил внимания на это его замечание (тогда я не знал, что это имеет ключевое значение, а потом не вспомнил). Он, кажется, единственный, кто на это указал.

Благодарю также авторов всех остальных ответов на этот вопрос. Их прислали: Ivan Nevraev, Alexander N. Dovzhikov,Alex Hin.

/ / / В ПОИСКАХ ИСТИНЫ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Q. Не подскажете как в tray выводить текст, как например сделаны часы в windows?

- Dmitriy

Как выводить в tray иконку, надеюсь, все знают ;)
Shell_NotifyIcon( ) есть, а вот Shell_NotifyText( ), к сожалению, не существует... ;)

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

У меня просьба (в связи с небольшими неполадками) - прошу тех, кто не получил от меня ответа в течение недели или больше, послать письмо еще раз.

Желаю всем программировать с удовольствием!

©Алекс Jenter mailto:jenter@mail.ru
Красноярск, 2000.


No.11

Программирование на Visual C++

выпуск No.11 от 22/07/2000

Добрый день всем!

В ответ на публикацию вопроса Дмитрия о System Tray в предыдущем выпуске помимо прямых ответов пришло еще несколько просьб рассказать о том, как в системный tray вообще помещать иконки. Я, видимо, был излишне оптимистичен, когда посчитал, что это все знают ;) Так что я решил поведать уважаемым читателям об этом в данном выпуске, в рубрике "WINAPI", в расчете на то, что эта информация будет полезна многим. Получается, сегодняшний выпуск целиком посвящен system tray ;)

/ / / / WINAPI / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Итак, задача у нас следующая: поместить в системный tray свою иконку, причем заставить ее функционировать стандартным образом - чтобы при наведении на нее появлялась подсказка, при нажатии на правую кнопку мыши выскакивало меню, на левую - производилось какое-нибудь действие.

Начнем с начала - нужно поместить иконку в tray. Сами вы это вряд ли сделаете - да это и не нужно. За вас это сделает Windows, вам нужно только сообщить операционной системе о своем намерении. Для этого служит функция Shell_NotifyIcon( ), которая позволяет создавать, изменять и удалять такие иконки.

Первый аргумент этой функции это код операции, которую вам нужно осуществить. Он имеет три возможных значения - NIM_ADD, NIM_DELETE и NIM_MODIFY. В пояснениях, по-моему, не нуждается. Второй параметр - указатель на структуру NOTIFYICONDATA. Вот как эта структура выглядит:

typedef struct _NOTIFYICONDATA {
      DWORD cbSize; // размер, обязательно указывать
      HWND hWnd;    // HWND для посылки уведомлений
      UINT uID;     // идентификатор иконки в tray, не имеет отношения к ресурсам
      UINT uFlags;  // см. ниже
      UINT uCallbackMessage; // посылается вашей функции окна
      HICON hIcon;    // дескриптор иконки
      CHAR szTip[64]; // строка с подсказкой
} NOTIFYICONDATA;

// uFlags
#define NIF_MESSAGE 0x1 // uCallbackMessage содержит информацию
#define NIF_ICON 0x2    // hIcon содержит информацию
#define NIF_TIP 0x4     // szTip содержит информацию

В принципе, назначение каждого члена этой структуры довольно прозрачно. Замечу только, что uID - это не идентификатор ресурса иконки, как можно было бы подумать, а вами определенный идентификатор для tray icon вашего приложения. Иконка, которую выводит в tray приложение, может меняться в процессе работы, но этот идентификатор остается постоянным.
Также вам нужно в uCallbackMessage записать сообщение, которое вы хотите чтобы система вам посылала в качестве уведомления о событиях, происходящих с вашей иконкой. Для этого в программе определите какое-нибудь user-defined сообщение, например так: #define WM_TRAYNOTIFY (WM_APP+100)
WM_APP используется как раз для того, чтобы именно в таком виде и определять нужные вам сообщения.

Теперь, предположим у вас подготовлена иконка для tray: IDI_MYTRAYICON. Нам нужно ее вывести в tray. Вот что мы делаем:

// уведомляющее сообщение
#define WM_TRAYNOTIFY (WM_APP+100)

// идентификатор иконки
#define ID_TRAYICON 1000

...
CString sNotifyTip = "Название вашей программы или другая подсказка";

NOTIFYICONDATA nid;

memset (&nid, 0, sizeof(nid) ); // обнулять структуру перед использованием - хорошая привычка

nid.cbSize = sizeof(nid);
nid.hWnd = hWnd;
nid.uID = ID_TRAYICON;
nid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP;
nid.uCallbackMessage = WM_TRAYNOTIFY;
nid.hIcon = ::LoadIcon (hInstance, MAKEINTRESOURCE(IDR_MAINFRAME) );
lstrcpyn (nid.szTip, sNotifyTip, sizeof(nid.szTip) );

Shell_NotifyIcon ( NIM_ADD, &nid );

Этот код вставьте в функцию инициализации, причем окно вашего приложения уже должно быть создано, hWnd и hInstance должны быть определены. hWnd вы получаете при создании окна, а hInstance вам передают прямо в WinMain. Если у вас MFC-приложение, поставьте вместо них соответственно AfxGetMainWnd()->m_hWnd и AfxGetApp()->m_hInstance.

Ну вот, иконку мы вывели, и даже подсказка у нас выводится. Для своевременного удаления иконки в функцию, обрабатывающую выход из программы, поставьте примерно такую же конструкцию, но с NIM_DELETE:

NOTIFYICONDATA nid;

memset (&nid, 0, sizeof(nid) );

nid.cbSize = sizeof(nid);
nid.hWnd = hWnd;
nid.uID = ID_TRAYICON;

Shell_NotifyIcon( NIM_DELETE, &nid );

(в структуре nid достаточно теперь определить только cbSize, hWnd и uID).

Но иконка бесполезна, если она ничего не делает. Давайте добавим немного функциональности. Система посылает нам сообщение WM_TRAYNOTIFY каждый раз, когда с иконкой что-то происходит. Все, что мы должны сделать - обработать это сообщение и отреагировать должным образом.

Добавьте в программу обработчик события WM_TRAYNOTIFY. В этом сообщении wParam - это ID иконки, а lParam - код сообщения от мыши, например WM_RBUTTONDOWN. Если у вас не MFC-приложение, просто добавьте один case в функцию окна. Если же вы имеете дело с MFC, то сделайте следующее: в класс главного окна(диалога) добавьте функцию afxmsg void OnTrayNotify ( WPARAM wParam, LPARAM lParam );

В карту сообщений класса добавьте следующую строку: ON_MESSAGE ( WM_TRAYNOTIFY, OnTrayNotify )
Таким образом обрабатываются пользовательские сообщения. Эта строка свяжет наше сообщение WM_TRAYNOTIFY с функцией его обработки OnTrayNotify( ).

В этой функции проверяйте значение lParam и делайте то, что вам нужно, например, выводите меню. Как именно это делать - уже совсем другая история...

void CMyDlg::OnTrayNotify(WPARAM wParam, LPARAM lParam)
{
  if (lParam==WM_LBUTTONDOWN)
  {
    ::SetForegroundWindow(m_hWnd); // активизируем наше приложение
    AfxMessageBox("Была нажата левая кнопка");
  }
  else
  if (lParam==WM_RBUTTONDOWN)
  {
    ::SetForegroundWindow(m_hWnd);
    AfxMessageBox("Была нажата правая кнопка");
  }
}

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

Q. Не подскажете как в tray выводить текст, как например сделаны часы в windows?

- Dmitriy

A1. Copy from ListSOFT от 18.07.2000

"...Если хочешь, чтобы рядом с системными часами располагалась надпись, например, твое имя, то в HKEY_CURRENT_USER\ Control panel\ International\
в первые два параметра запиши его (не более 8 символов), а в третий запиши "HH:mm:ss tt". Кстати, если изменить формат времени таким способом, то строка, записанная в первые два параметра будет фигурировать во всех программах, запрашивающих время, например, в Outlook Express в графе Отправлено и Получено."

- Grigori Zagarski

Я попробовал так сделать - не получилось. У меня в этом разделе вообще всего один параметр - "Locale". Что-то автор напутал... Может, путь указан неправильно? Хоть результат и отрицательный, я решил все же на всякий случай опубликовать этот ответ - может, тут действительно дело во мне (я проверял в Windows 98SE), ведь на ListSOFT действительно была такая публикация. А может, кто и подскажет, в чем дело.

A2. Я предлагаю набирать текст из иконок, которые должны создаваться динамически.
Подобный подход я видел в нескольких программах, где tray-иконки используются для индикации уровня занятости CPU и т.п.
Пример вывода текста в tray приаттачен. Сам вывод делается в классе CShellNotifyText. Тестовая программка организует в tray'е что-то типа таймера.
Может быть это не самый простой вариант, но пусть кто-нибудь предложит лучше :))
Хочу высказать свое положительное мнение о рассылке. Единственное, что смущает в свете последних известий от Microsoft: Кому будут нужны знания по MFC, когда все начнут программировать на Си-диез (C#)?

- Сергей Цивин

Пример я посмотрел, он работает. Но, к сожалению, у такого подхода есть один очень существенный недостаток: если TaskBar в высоту имеет больше одной полосы , никто не гарантирует, что у вас не произойдет переноса на самом неподходящем для этого символе. Я сам смоделировал такую ситуацию, это было сделать легко и выглядело совершенно неприемлемо. Если кто-нибудь знает вдруг, как эту дилемму разрешить - пишите.
А насчет C# - вынужден повториться, он не позиционируется как конкурент VC и MFC. Microsoft полагает, что это Java-киллер. Так, в следующую версии VisualStudio известный продукт Visual J++, скорее всего, не войдет, а вместо него будет сами догадайтесь что...

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Успехов!

©Алекс Jenter mailto:jenter@mail.ru
Красноярск, 2000.


No.12

Программирование на Visual C++

выпуск No.12 от 24/07/2000

Приветствую!

/ / / / MFC / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Недавно мне пришло письмо с просьбой рассказать о таком элементе управления, как CTabCtrl. После того, как я отправил ответ, я подумал, что это могло бы быть интересно многим. Так что я немного переработал материал, кое-что добавил и - читайте!

CTabCtrl: Закладки как средство продуманного интерфейса

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

Во тут-то нам и приходят на помощь закладки. Они позволяют иметь несколько страничек, и легко между ними переключаться. Посмотрите - в Windows очень много примеров применения такого подхода. Наверняка вы с ним уже встречались, и не раз.

Итак, какие же средства предоставляет нам MFC для работы с закладками? Можно назвать три класса: CPropertySheet (вместе с CPropertyPage) и CTabCtrl.

Первый класс (CPropertySheet) представляет собой более сложное образование, позволяющее создавать так наз. страницы свойств, готовые диалоги со стандартными кнопками и набором закладок, где вы размещаете свои элементы управления. В качестве примера можете посмотреть диалог Tools|Options в интегрированной среде Visual C++. Это полезно, если вам нужно создать именно такой диалог, например для изменения конфигурации программы. CPropertySheet представляет набор страниц, CPropertyPage - отдельную страницу такого набора.

Но что если вам нужно получить больший контроль над закладками? Что если вам нужны только закладки, а не готовый диалог? А еще если вы хотите кроме текста в заголовках закладок рисовать иконки?

Вот тогда вам нужно воспользоваться CTabCtrl, классом более низкого уровня, чем CPropertySheet. Замечу, что сам класс CPropertySheet использует CTabCtrl, причем его можно попросить дать вам указатель на этот объект. Таким образом, Узная, как работать с CTabCtrl, вы одновременно узнаете, как можно на низком уровне работать с CPropertySheet. А про CPropertySheet я расскажу как-нибудь в другой раз.

Пусть вам нужно сделать закладки в существующем диалоге. Создать элемент типа CTabCtrl можно двумя способами: динамически (в программе) и в редакторе ресурсов. Для примера воспользуемся вторым способом.
В палитре элементов найдите "Tab Control" и поместите его в ваш диалог. Теперь два раза кликните по нему мышкой при нажатой клавише Ctrl. Вам будет предложено создать переменную класса, соглашайтесь. Введите m_Tab в качестве имени и CTabCtrl в качестве типа.

По умолчанию наш объект пока не содержит ни одной закладки. Чтобы они появились, их необходимо создать с помощью функции InsertItem( ). Это можно сделать в OnInitDialog( ):

BOOL CTabDlg::OnInitDialog()
{

  TC_ITEM tci;   // в нее записываются параметры создаваемой закладки

  memset(&tci,0,sizeof(tci));
tci.mask = TCIF_TEXT;    // у
закладки будет только текст

tci.pszText = "Закладка 1";
// название закладки
m_Tab.InsertItem(0, &tci); // первая
закладка имеет индекс 0

tci.pszText = "Закладка 2";
m_Tab.InsertItem(1, &tci);  //
вставляем вторую закладку

return TRUE;
}

Ну вот, у нас есть две закладки. Теперь нам нужно поместить что-нибудь внутрь.

Прежде всего, для каждой из закладок нужно создать диалог, который будет отображаться при выборе этой закладки. Например, создайте для начала два диалога - IDD_TABPAGE1 и IDD_TABPAGE2. В свойствах каждому поставьте тип "Child" - "дочерний" (properties|styles|style:Child) и "Без рамки" (properties|styles|border:None). Для каждого диалога нужно создать соответствующий класс. Это можно сделать, два раза кликнув по поверхности диалога в редакторе. У меня получились классы СTabPage1 и CTabPage2.

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

В классе вашего диалога, кому принадлежит TabCtrl ( в примере- CTabDlg) добавьте переменную-указатель на текущий диалог:

protected:
 CTabCtrl m_Tabs;
 CDialog* m_pTabDialog; // <--- добавить

В конструкторе класса проинициализируйте ее в 0:

CTabDlg::CTabDlg(CWnd* pParent /*=NULL*/)
   : CDialog(CTabDlg::IDD, pParent)
{
  m_pTabDialog=0;
}

Зайдите в ClassWizard и для TabCtrl добавьте обработчик TCN_SELCHANGE (изменение закладки).
Теперь мы будем динамически удалять прошлый диалог/ создавать новый и выводить его в TabControl.

Вот как это выглядит:

void CTabDlg::OnSelchangeTab1(NMHDR* pNMHDR, LRESULT* pResult)
{
  int id; // ID диалога

  // надо сначала удалить предыдущий диалог в Tab Control'е:
  if (m_pTabDialog)
  {
    m_pTabDialog->DestroyWindow();
    delete m_pTabDialog;
  }

  // теперь в зависимости от того, какая закладка выбрана,
  // выбираем соотв. диалог
  switch( m_Tab.GetCurSel()+1 ) // +1 для того, чтобы порядковые номера закладок
                                // и диалогов совпадали с номерами в case
  {
    // первая закладка
    case 1 :
          id = IDD_TABPAGE1;
          m_pTabDialog = new CTabPage1;
            // тип указателя соответствует нужному диалогу,
            // иначе добавленный в диалог код не будет функционировать
    break;

    // вторая закладка
    case 2 :
          id = IDD_TABPAGE2;
          m_pTabDialog = new CTabPage2;
    break;

    // все остальные закладки, если они есть,
    // будут здесь тоже представлены, каждая - отдельным case

    // а если обработка такого номера не предусмотрена
    default:
       m_pTabDialog = new CDialog; // новый пустой диалог
       return;

   } // end switch

  // создаем диалог
  m_pTabDialog->Create (id, (CWnd*)&m_Tabs); //параметры: ресурс диалога и родитель

  CRect rc;

  m_Tab.GetWindowRect (&rc); // получаем "рабочую область"
  m_Tab.ScreenToClient (&rc); // преобразуем в относительные координаты

  // исключаем область, где отображаются названия закладок:
  m_Tab.AdjustRect (FALSE, &rc);

  // помещаем диалог на место..
  m_pTabDialog->MoveWindow (&rc);

  // и показываем:
  m_pTabDialog->ShowWindow ( SW_SHOWNORMAL );
  m_pTabDialog->UpdateWindow();

  *pResult = 0;
}

Теперь последний штрих: в OnInitDialog( ) нужно добавить следующий код:

...
 m_Tab.InsertItem(1, &tci);

 //-----------------
 // добавить:

 NMHDR hdr;

 hdr.code = TCN_SELCHANGE;
 hdr.hwndFrom = m_Tab.m_hWnd;

 SendMessage ( WM_NOTIFY, m_Tab.GetDlgCtrlID(), (LPARAM)&hdr );

 //-----------------

 return TRUE;
}

Это необходимо для того, чтобы отобразить самую первую закладку.

Как вариант можно просто вызвать OnSelchangeTab1(0,0); но тогда из OnSelchangeTab1 нужно удалить последнюю строку (*pResult=0).

Можете вволю поэксперементировать со свойствами и стилями CTabCtrl. Мне, например, очень нравятся закладки, надписи на которых подсвечиваются при наведении курсора мыши, кстати это имеет место в MS Access 97 (стиль TCS_HOTTRACK).

И еще: не забудьте, если диалог у вас немодальный, вы должны обеспечить корректный обмен данными между активным диалогом в Tab Control и вашим приложением. Это делается точно так же, как и обычный обмен данными с немодальным диалогом.

/ / / / ОБРАТНАЯ СВЯЗЬ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Небезызвестный вам Борис Бердичевский (см. выпуск No. 3) делится своим решением часто возникающей проблемы с сериализацией.

Приветствую!

Случилось так, что мне пришлось потратить целый рабочий день на поиск одного решения в области сериализации; готов поделиться.
Как я уже отмечал недавно, при сериализации данных нужно решать проблему совместимости старых структур сохраняемых данных, т.е. если вы добавили новое поле в какой-то класс, подлежащий сериализации, будьте добры, обеспечьте чтение этого класса, ранее сохраненного без этого поля.
Способ, который я приводил (ReadClass/WriteClass), вполне хорош. Но закавыка-то в том, что в предыдущей версии я по какой-то причине (просто по неопытности) сохранял без WriteClass! А имплементация сериализации была описана как IMPLEMENT_SERIAL(MyClass, CObject, 0)

Итак, подобным образом сериализованный класс надо было успешно прочитать. Понятное дело, ReadClass на такую сериализацию вызывает CArchiveException с кодом CArchiveException::badIndex (=5, для справки)

Казалось бы, лови CArchiveException и обрабатывай себе, но не тут-то было! Вроде незначительная проблема: указатель архива продвигается, и невозможно уже прочитать данные из-за смещения. Никакого средства для возврата указателя на место для CArchive не существует! (Я бы мог попросить уважаемых читателей порыться, но заранее заявляю: бессмысленно! Говорите, можно манипулировать с функцией Seek принадлежащего
CArchive классу CFile? - пробовал, не работает.)

По счастью, нашлась недокументированная возможность. Выяснилось, и это очень важный нюанс, который не разъяснен в MSDN: У функции CReadClass есть 3 параметра, 2 последних имеют умолчание NULL:

CRuntimeClass* ReadClass( const CRuntimeClass* pClassRefRequested = NULL,
                     UINT* pSchema = NULL, DWORD* obTag = NULL );
Throw CArchiveException;
Throw CNotSupportedException;

Первый параметр - RuntimeClass для проверки на соответствие загружаемого класса.
Второй параметр - поинтер на UINT - версию сериализуемого класса.
Третий параметр, и это самое интересное (кстати, в MSDN записано, что, он предназначен для внутреннего использования в функции ReadClass и обычно задается как NULL) - если не задан нулем, возвращает в младших 2-х байтах значение, сериализованное из архива, а CArchiveException при этом не вырабатывается! Версия при этом не заполняется.

Отсюда решение, которое проиллюстрировано во фрагменте кода:

#define BASE_DATA_VERSION 0x100

IMPLEMENT_SERIAL(MyClass, CObject, VERSIONABLE_SCHEMA | BASE_DATA_VERSION)

void MyClass::Serialize( CArchive& ar)
{
  UINT Version=NULL;
  CObject::Serialize(ar);
  DWORD Tag;
  if(ar.IsLoading())
  {
   TRY
    {
      ar.ReadClass(RUNTIME_CLASS(MyClass), &Version, &Tag);
      if( Version == BASE_DATA_VERSION)
           ar >> dwValue; // описано в классе DWORD dwValue;
      else
      {
        WORD HighW;
        ar >> HighW;
        dwValue = MAKELONG(Tag, HighW);
      }

   ar.Read( title, sizeof(title)); // описано в классе
         // char title[LEN_TITLE];
   ar >> val1;
   ar >> val2; // нормальная сериализация
...

- Борис Бердичевский

___________________

Здравствуйте Алекс!

Только что подписался на вашу рассылку, и прочитал все выпуски из архива. Мне показался интересным вопрос из выпуска N2 про возможность структурного сохранения данных в MFC.
На мой взгляд опубликованные ответы были не очень точны, т.к. в MFC все-таки имеются некоторые способы такого хранения, хотя конечно не совсем до конца доделанные.
Прежде всего существует класс COleDocument, который позволяет хранить данные в compound file - в которых как раз и хранятся иерархические данные.
У этого класса есть, к сожалению не документированный, член m_lpRootStg который представляет из себя корневой storage документа. К сожалению в MFC нет стандартных средств работы со storage, но никто не мешает пользоваться его собственным интерфейсом.
Зато есть класс COleStreamFile который инкапсулируют в себе IStream являясь в то же время потомком CFile, что делает возможным его использования для CArchive и соответственно serialize.
И наконец последнее замечание. К сожалению, модель MFC такова, что при использовании своих IStorage объектов, их НЕОБХОДИМО записывать в функции serialize, иначе они могут потеряться при команде Save As.

Вот и все что я хотел напистаь. Может немного сумбурно и поздновато, но может кому-нибудь пригодиться.

С уважением,

- Nick Pisanov

Спасибо Нику за ответ и полезную информацию. Я просто хочу заметить, что все-таки вместо "недоделанных" способов чаще предпочитаю использовать свои, хоть с потом и кровью созданные, но доделанные, удобные, не основывающиеся на недокументированных возможностях, досконально известные и работающие на все 100%.
Но это вопрос философский, конечно... Иногда действительно это приводит к изобретению велосипеда. Программирование - это все-таки больше искусство, чем наука ;) Каждый творит по-своему.

___________________

Пришло дополнение к прошлому выпуску:

Hello!

Маленькое замечание. После Shell_NotifyIcon ( NIM_ADD, &nid ); надо еще добавить ::DestroyIcon( nid.hIcon );

Я как-то делал анимацию иконки в трэе (типа как TheBat крылышками там машет) и долго не мог понять, почему после нескольких часов работы прога вешает всю систему, а никакой утечки памяти нет.

- Андрей, Норильск

Огромное спасибо, Андрей! Действительно, если делать такие анимации в tray, то своевременное уничтожение иконки становится критичным.

И еще на тему прошлого выпуска:

В 11 выпуске был вопрос, касаемо часов и реестра. Дело не в тебе, а в авторе сообщения. Дело в том, что windoza ставит в этот ключ что-то только если user что-то поменял в разделе "язык и стандарты". Если же там всё по умолчанию, то в реестре у тебя будет по этому пути только параметр Locale, указывающий код страны, правила записи даты, времени и т.п. которой используются для вывода системной даты.

- Пригожев Александр (alexproger)

Что ж, видимо так оно и есть. Я дописал нужные параметры и все заработало. Но только после того, как я изменил региональный стандарт с русского на английский(США). Те два параметра, в которых вы записывали свое имя - на самом деле это метки "до полудня" и "после полудня ", по умолчанию равные "AM" и "PM". В русском стандарте эти метки не используются.

 / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

ПРОШУ ВНИМАНИЯ :

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

Надеюсь, что рассылка для вас является интересной и познавательной. Также уверен, что после выхода из отпуска она станет еще полезнее, так как я собираюсь воплотить несколько новый идей. А еще осенью должен открыться официальный сайт рассылки. Так что

до встречи в новом сезоне! Оставайтесь с нами!

©Алекс Jenter mailto:jenter@mail.ru
Красноярск, 2000.


No.13

Выпуск No. 13 от 7 сентября 2000 г.

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

Чрезвычайно рад приветсвовать вас после чуть более чем месячного отдыха. Как вы уже наверное сами догадались, рассылка вышла из отпуска и снова будет периодически радовать вас всяческой полезной информацией о мощнейшем современном инструменте разработки, а именно Microsoft Visual C++, ну а также о всем, что с ним связано. Начинается новый сезон выпусков, и начинается он с весьма счастливого номера - тринадцатого ;) Впрочем, я никогда не был особенно суеверным, так что пропускать это замечательное число никак не намерен.
Я взял на себя смелость немного изменить оформление рассылки - спасибо ребятам с subscribe.ru, они наконец-то догадались, что далеко не все пребывают в диком восторге от их цвета для фона страницы HTML-варианта, который мне почему-то всегда напоминал цвет известного сельскохозяйственного удобрения. Теперь это можно изменить, что я, особо не мешкая, и сделал. Впрочем, как говорится, на вкус и цвет товарища нет, так что будем условно считать возможную дискуссию на эту тему несостоявшейся.

За время моего отсутствия пришло много писем с вопросами и предложениями, и я думаю, что самые интересные из них мы разберем в ближайших выпусках.

Есть, к сожалению, и не очень приятные новости. Сейчас у меня будет оставаться не так много времени на рассылку, как раньше. Выпуски будут выходить так же регулярно, но, возможно, самую малость пореже. И еще: я не буду делать отдельную текстовую версию, придется положиться на конвертер сервера. Прошу прощения у подписчиков текстового варианта. Понимаю, что звучит банально (я такие утверждения встречал почти в каждой рассылке, а теперь дошел до того, что пишу это сам) но HTML-вариант ДЕЙСТВИТЕЛЬНО гораздо лучше выглядит и занимает ненамного больше времени для загрузки.

/ / / M F C / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

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

НАБОР ЗАКЛАДОК: ПАРА ЗАМЕЧАНИЙ

Описанный мной в #12 способ организации закладок в принципе работает, но имеет два существенных недостатка, на которые обратил внимание мой хороший друг, программист Bad Sector. Одновременно он подсказал пути их устранения.

Недостаток первый: в фокусе ввода закладки, при нажатии клавиши "Enter" не происходит передача сообщения родительскому окну, содержащему набор, а идет его обработка "на месте". Это может, в худшем случае, привести к удалению содержимого текущей закладки с экрана, а кнопка по умолчанию родительского диалога не сработает. Сразу скажу, что проблема разрешается перекрыванием PreTranslateMessage(), но есть способ проще и лучше, который я опишу чуть ниже.

Второй недостаток гораздо серьезнее: из-за того, что объекты-страницы закладок у меня создаются и удаляются динамически во время переключения, состояние их элементов не сохраняется. Это не имеет значения, если у вас там одни кнопки ;) но чаще всего бывает как раз наоборот.

Помните, я вам обещал рассказать про классы CPropertySheet и CPropertyPage? Время первого еще не пришло, а вторым мы сейчас как раз и займемся. Потому как использование его вместо CDialog в качестве родительского класса для наших страниц-закладок МОМЕНТАЛЬНО снимает первую проблему. Сообщение будет передаваться куда надо и как надо. Отметьте, что CProperyPage сама наследует от CDialog, и своим поведением отличается от него в таких вот ситуациях. А еще обычно как-то упускается из виду, что CPropertyPage можно использовать отдельно, а не в связке с СPropertySheet.

Таким образом, первый недостаток устранен, перейдем ко второму. Здесь тоже все несложно: необходимо выбрать - либо вы будете хранить настройки каждой из закладок в содержащем их диалоге, и при переключении каждый раз их сохранять/загружать (путь мазохиста). Либо же вы все нужные закладки создадите заранее (в массиве, например), при открытии содержащего их диалога. При его же закрытии, вы, если это необходимо, все нужные данные из классов закладок можете скопировать в отдельные переменные, а затем со спокойной совестью удалить весь набор. В таком случае переключение закладок будет выглядеть следующим образом:

    CPropertyPage
*m_Pages[];
CTabCtrl m_Tabs;
int  m_iLastPage=-1;
...
void
CMyDlg::OnSelChangeTab(NMHDR* pNMHDR, LRESULT* pResult)
{
if (m_LastPage !=  -1)
m_Pages[m_iLastPage]->ShowWindow(SW_HIDE);

int c =
m_Tabs.GetCurSel();
m_Pages[c]->ShowWindow(SW_SHOW);
m_Pages[c]->UpdateWindow();
m_iLastPage = c;

*pResult
= 0;
}

 / / / ОБРАТНАЯ СВЯЗЬ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Еще письмо на тему отказа работать release-версии программы. И прошу не ворчать, потому что когда сами с такой проблемой встретитесь (скорее всего, когда программу показывать нужно уже через несколько дней), скажете этому человеку спасибо!

Сам недавно столкнулся с "проблемой Release билда". Естественно, 95% всех подобных багов - это баги с памятью. Проблемы с NULL указателями ещё менее-более отслеживаются, а вот с "перетиранием" памяти - сложнее.
Я решил эту проблему с помощью _CrtCheckMemory(). Вообще, в Windows есть минимальный набор механизмов для отладки, они все описаны в MSDN 'Debug Routines'.

Т.е. сначала определяем с помощью внутреннего логгинга _примерное_ место вылета проги - это нужно делать не в Debug build (там-то всё работает :), а в Release. Простой лог можно за полчаса набросать. Если прога не использует DirectX или OGL, то можно (наверное), использовать даже MessageBox. Главное - локализовать место появление бага. Надо сразу заметить, что, как правило, место, где прога вылетает, совсем не то же самое место, где есть баг. В моём случае баг был в инициализации, а валилась функция уже во время работы. Потом работаем с помощью assert( _CrtCheckMemory()); уже в Debug build, где есть дебаггер и всё такое.

Если программа менее-более линейна и не сильно здоровая, то можно вставить assert( _CrtCheckMemory()); прямо по ходу выполнения, перед и после каждого подозрительного вызова. Он сработает, как только обнаружит поврежденный heap - мы сразу можем видеть где это происходит, и делать выводы. Надеюсь это хоть кому то поможет.

- Иван Невраев

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Ну вот и все на сегодня. До новых встреч и будьте здоровы!

©Алекс Jenter mailto:jenter@mail.ru
Красноярск, 2000.


No.14

Выпуск No. 14 от 14 сентября 2000 г.

Приветствую вас!

Выпуск чуть-чуть задержался, в основном из-за суеты этих сентябрьских дней. Но, надеюсь, что вы еще не успели заскучать.

 / / / ОБРАТНАЯ СВЯЗЬ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Предлагаю вашему вниманию интересное письмо, пришедшее пока я был в отпуске.  В нем затрагивается очень больная тема для всех MFC-программистов:

Я только что подписался на Вашу рассылку и прочитал весь архив. В первую очередь хочу присоединиться ко всеобщим поздравлениям (хоть и с нескольким опозданием) и поблагодарить Вас за то что принялись за столь нелегкий и, насколько я понимаю, практически бесприбыльный труд. Так уж получилось, что в этой области работает очень много профессионалов и любителей, поэтому вопросов накопилось очень много. Я крайне рекомендую Вам почаще направлять вопрошающих на microsoft.public.ru.vc и microsoft.public.ru.russian.programming где подобные вещи более уместны. Хоть это и самые активные русскоязычные конференции на тему, но они недотягивают то своих западных конкурентов, поэтому свежая кровь им явно не повредит.
[...] К делу: Может я и ошибаюсь, но насколько я знаком с психологией Microsoft, можно судить, что MFC доживает свои последние дни. Она морально устарела, скорее всего эта библиотека уйдет в небытие "оставлено для совместимости" уже со следующей версией VS. Я очень хочу, чтобы Вы в своей рассылке обратили внимание на WTL (Windows Template Library) - это библиотека шаблонов похожая на ATL, способная частично (или полностью) заменить MFC. Она входит в Platform SDK начиная с Jan'2000. Пока это пробный камень, поэтому практически недокументирована. Microsoft не слишком афиширует ее появление, и те немногие программисты, которые ее используют, вынуждены разбираться во всем самостоятельно. А выгод при ее использовании очень много. Например она значительно дружественнее к WinAPI, который активно рассматривается в рассылке, чем MFC. Возможно с выходом следующей версии VS WTL будет дополнена или даже изменена и выставлена как основное средство разработки приложений в VC, так как больше отвечает предназначению C++ - созданию компактных, быстрых и эффективных приложений. Именно по этим причинам я считаю очень полезным рассмотреть эту библиотеку в рассылке, а в будущем, возможно, уделить ей больше внимания чем MFC.

- Ярослав Говорунов

Итак, есть два вопроса. Вопрос первый - что будет с MFC в будущем? Вопрос второй : что это еще за зверь - WTL?

На первый этих двух вопросов не существует однозначного ответа. Если я разверну дискуссию на эту тему в рассылке, то наверное вы не скоро дождетесь ее окончания, настолько это острый вопрос. Скорую смерть MFC предсказывали не раз и не два, но почему-то эта смерть все никак не наступит. Даже наоборот, сейчас трудно найти объявление о найме программиста на С++ под Windows, где не требовалось бы знание MFC (и чаще всего еще и ActiveX/COM). Работодатели задают тон, и поэтому MFC и сейчас так же популярен, несмотря на всю свою нелогичность, неудобство, малонадежность и множество других недостатков. Наверное, пока что его доствоиства (а они, надо признать, все-таки есть) плюс усилия всемогущей Microsoft по его поддержке перевешивают. Да и в обозримом будущем, скорее всего, ситуация мало изменится - в следующую версию Visual Studio (о которой я писал в выпуске No. 8) MFC, вне всякого сомнения, войдет. Будет ли это в виде "оставлено для совместимости"? Я думаю, вряд ли. MFC cлишком уж широко используемая библиотека. Хотя это, конечно, не более чем мое личное мнение.

А вот второй вопрос действительно интересен. Неужели появилась достойная альтернатива MFC? Чтобы каждый из вас сам ответил для себя на этот вопрос, хочу предложить вашему вниманию статью Ричарда Граймса, на которую я наткнулся в интернете, и она мне настолько понравилась, что я решил специально для вас ее перевести и опубликовать. Что я и делаю с любезного разрешения автора статьи.

/ / / СТАТЬЯ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

ЧТО ТАКОЕ WTL?

Автор: Ричард Граймс
Источник: iDevResource.com Ltd.
Оригинал: "What is WTL?" by Richard Grimes
Пер. с англ. Алекс Jenter

Вступление

О WTL шепчут уже более года, и был даже пущен слух, что эта библиотека используется внутри самой Microsoft, и что она базируется на ATL. Конечно же, это привлекло внимание сообщества ATL-разработчиков, которые создавали пользовательский интерфейс для элементов управления ATL еще со времени появления ATL 1.1, и обнаруживали, что код, который они писали, был большей частью чистым кодом Win32 GDI. Я могу кое-что вам сообщить: WTL построен по такому же принципу.

Является ли это разочарованием? Нет, потому что сама ATL - всего лишь тонкая обертка COM, и в этом-то и заключается ее сила. Конечно, вам необходимо знать COM для того, чтобы использовать ATL, но дополнительные усилия, затраченные на изучение ATL пренебрежимо малы по сравнению с теми, которые нужны для освоения COM. Сравните это с другими библиотеками классов, где основной упор делается на изучение самой библиотеки, а что вы фактически будете знать по окончании обучения? Не так уж много о COM, это определенно.

С WTL все в принципе так же. Вы должны уметь программировать, используя Win32 и GDI. Но если вы это знаете, тогда WTL для вас - не более чем глоток свежего воздуха. Если же вы не имеете представления о Win32 и GDI, тогда лучше вам писать пользовательский интерфейс на VB.

Что включает в себя WTL?

Библиотека имеет основной набор классов для приложения. Заметьте, что хотя у вас нет классов-документов (documents), как в MFC, у вас все еще есть классы-представления (views). В WTL очень много кода, предназначенного для того, чтобы позволить вам манипулировать представлениями, а также легко добавлять ваш собственный код. Существует свой мастер AppWizard, с помощью которого можно легко создавать каркасы SDI-, MDI- и многопоточных SDI-приложений (т.н. Multi-SDI-приложение выглядит, как будто открыто много экземпляров обычного SDI-приложения, но на самом деле это разные окна одного и того же процесса. Примером такого приложения может служить IE или Windows Explorer). Плюс к этому, ваша программа может быть приложением на основе диалога (dialog-based) или на основе представления (view-based). Сами представления могут быть основаны на классе CWindowImpl, на каком-либо элементе управления, или даже на HTML-странице. Вы также можете выбирать, будет ли ваше приложение иметь панель инструментов в стиле IE (rebar), в стиле Windows CE (command bar), или простую (toolbar); можно добавить строку статуса (status bar). Ваше приложение может внедрять элементы управления ActiveX и может быть COM-сервером.

Есть выбор среди нескольких видов классов-представлений, которые вы можете использовать. WTL представляет классы окон с разделителями (splitter-window), так что вы можете иметь два окна в одном представлении, и классы окон с прокруткой (scroller-window), где окно может быть меньшего размера, чем представление, которое оно отбражает. Существует также некий аналог UpdateUI из MFC, хотя в WTL он работает немного по-другому - основное отличие в том, что вы сами указываете, какие элементы могут обновляться посредством карты сообщений (message map), и вы должны добавить код в ваш класс, чтобы выполнить UpdateUI. Библиотека поддерживает технологии DDX/DDV, которые, опять же, очень похожи на их аналоги из MFC, с той только разницей, что у вас есть карта сообщений, которая реализует DoDataExchange и вам нужно добавлять код для осуществления этой операции.

Присутствуют теперь и классы GDI. Класс-оболочка HDC очень похож на CWindow в том, что очень тонок, - добавляет мало новой функциональности. Тем не менее, в нем есть поддержка метафайлов и OpenGL. Я думаю, основное применение будут иметь классы-наследники для работы с принтерными контекстами устройства - в WTL есть поддержка печати и даже предварительного просмотра (print preview). Имеются также классы-обертки для GDI-объектов, кистей (brushes), перьев (pens), регионов (regions), и т.д.

Еще в библиотеке можно обнаружить классы для всех стандартных Win32 и W2K (Windows 2000) диалогов (common dialogs), опять же, хотя эти обертки довольно тонки, они делают задачу выбора шрифта или, скажем, файла действительно простой.

Старый файл AtlControls.h был включен из ATL в WTL, и содержит несколько новых классов для элементов управления W2K, наряду с некоторыми классами для элементов управления, не относящихся к Win32, таких как клон панели команд (command bar clone), кнопка с изображением (bitmap button), гиперссылка (hyperlink) и курсор "песочные часы" (wait cursor). [...]

И, наконец, в библиотеке имеются служебные классы, самым значимым из которых является CString. Да, это клон класса CString из MFC, который реализует (насколько я знаю) все его методы. Еще есть класс-оболочка для поиска файлов (find file API) и классы-аналоги CRect, CSize и CPoint.

Резюме

Если вы собираетесь писать Win32-приложение с пользовательским интерфейсом, я рекомендую вам попробовать WTL прежде чем думать об MFC. Если вы пишете код в WTL, он будет меньше в объеме и более эффективен, и вы будете иметь все преимущества поддержки COM в ATL, которая, увы, отсутствует в MFC. [ ]
/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Итак, WTL - очень перспективная библиотека на основе ATL, которая, однако, НЕ ИЗБАВЛЯЕТ вас, как программиста, от необходимости знания WinAPI. И я могу привести кучу доводов в пользу такой архитектуры, многие из которых достаточно очевидны. Но, как всегда, есть и аргументы contra. MFC намного мощнее, и имеет намного больше возможностей. Из них, в частности, модель "документ/представление", поддержка документов OLE, автоматизированный обмен данными, пристыковывающиеся панели/диалоги и многое другое.
 Объем кода, который вам приходится писать самому, в WTL больше.
Не думайте также, что в WTL нет ошибок: как показывает практика, их там тоже полным-полно (я видел список известных на данный момент багов). Библиотека еще сыровата. Так что не следует переоценивать WTL, но и спускать со счетов тоже не стоит. Конечно, она пока не стала стандартом и официально Microsoft не поддерживается. Но, как говорят на западе, things can change. В нашей профессии всегда приходится держать ухо востро на все инновации, т.к. они имеют неприятную особенность становиться стандартами.

А что касается рассмотрения WTL в рассылке: в принципе я не против, если вы не против. Смущает меня только одно: тем для рассылки становиться настолько много, что впору было бы создавать несколько рассылок, - одну про WinAPI, другую про MFC, третью про WTL, и т.д. Знаю, многие сочтут это просто отличной идеей, т.к. смогут подписаться именно на то, что их интересует. Но, к сожалению, я один, и физически не смогу все эти рассылки готовить, а помощников нет. Конечно, я буду стараться осветить самое важное и интересное. Как кто-то сказал, "нельзя объять необъятное... а если и можно, то только по частям и не сразу" ;) Так что давайте договоримся: пока будем придерживаться общепринятого стандарта - C++, WinAPI и MFC. А WTL (или С#, или чем-то другим) займемся, когда ее название будет фигурировать в объявлениях типа "требуется программист" чаще, чем MFC.

/ / / АНОНС / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
Читайте в следующем выпуске рассылки:
  • Как правильно писать программы на C++: некоторые правила, которые помогут вам писать легко читаемый код.
  • Рубрики "Вопрос-ответ" и "В поисках истины".

Также планируется в последующих выпусках:

  • CPropertySheet: создание окон свойств и мастеров.
  • Массивы, списки и ассоциативные списки. Общие положения и реализация в MFC.
  • Решение проблемы с OnIdle в dialog-based приложениях.
  • Работа с панелью инструментов. Задание вида кнопок и размещение отличных от кнопок элементов.
  • Постоянные рубрики "Вопрос-ответ", "В поисках истины" и "Обратная связь".
/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

До встречи!

;©Алекс Jenter mailto:jenter@mail.ru
Красноярск, 2000.


No.15

Выпуск No. 15 от 18 сентября 2000 г.

Доброе время суток!

Сегодня мы с вами поговорим о такой вещи, как стиль программирования.

/ / / / С++ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

ЛЕГКО ЧИТАЕМЫЙ КОД  И НАДЕЖНОСТЬ ПРОГРАММ

Если вам когда-нибудь приходилось разбираться в чужой программе, или даже в своей собственной, но написанной несколько лет назад, да еще когда поджимает время, вы скорее всего знаете, какая это неприятная и сложная задача. Порой бывает легче весь код написать заново (а многие так и делают). Конечно, существует поговорка "настоящие программисты не пишут комментарии: если программу было трудно написать, в ней должно быть трудно что-то понять и еще труднее что-либо изменить". О комментариях мы еще поговорим, печально то, что очень многие следуют этому принципу в самой программе. "Неважно", думают они, "как это написано, главное побыстрее проверить, работает ли это вообще". Самые прилежные из них впоследствии переписывают отдельные куски кода, но таких единицы. Большинство же, убедившись в работоспособности алгоритма, оставляют все как есть и переходят к новым задачам.
Как показывает многолетняя практика, это большая ошибка. Усилия, потраченные на "причесывание" кода, окупаются с лихвой. Но по-настоящему ценится умение писать сразу такой код, который не нуждается в причесывании. Это умение не приходит само по себе, а вырабатывается постепенно, в нем надо постоянно практиковаться. И когда вы говорите себе "а, это ведь просто небольшая утилитка для удобства, вот когда получу настоящий заказ - буду писать красиво", вы тем самым приучаете себя к неправильному стилю написания программ.

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

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

1. Обязательно соблюдайте отступы. Хотя Visual C++ и делает отступы автоматически, иногда они все же нарушаются. С их помощью сразу видна структура программы.
Кстати, многие знают, что для того, чтобы подвинуть блок текста вправо, нужно выделить его и нажать Tab, но почему-то даже не догадываются, что если нажать Shift-Tab, текст сдвинется влево! Попробуйте, это очень удобно. Лучше вместо символа табуляции использовать пробелы (Tools|Options|Tabs|Insert Spaces). Тогда ваши программы в любом редакторе будут с корректными отступами.

2. Про комментарии в коде я ничего говорить не буду... ну, почти ничего. Все, что можно было сказать, уже сказано до меня. Все равно лень людям их писать. Одно только вам посоветую: если уж сильно неохота сочинять комментарии 50/50 с кодом - все-таки постарайтесь самые ключевые и/или неочевидные моменты отмечать.
И запомните: неряшливый и запутанный код нужно не комментировать, а переписывать!

3. Именованные константы пишите в верхнем регистре, чтобы можно было мгновенно отличить их от переменных. Например, MAX_ELEMENTS и BORDER_WIDTH, а не Max_Elements и border_width.

4. Имена переменных начинайте с маленькой буквы, названия типов - с заглавной.

5. Глобальные переменные по написанию должны отличаться от обычных. Как правило, для этого используют префикс "g_": g_RefCount, g_BaseDir. Вообще, их количество следует минимизировать. Статические переменные можно обозначать суффиксом "s_", члены классов- "m_".

6. Переменным, имеющим длительный период существования, следует давать длинные имена. Локальным и временным переменным можно давать имена покороче.

7. Помните, что объект всегда подразумевается, т.е. не нужно повторять имя объекта в его методе. Например, MyObject->GetObjectColor() - эту функцию следует назвать просто GetColor().

8. Вкладывайте смысл в имена функций. Используйте слово "find" когда где-то что-то ищется, "get" когда что-то хотите получить, "set"- установить. "Initialize" или "init" - инициализация, "compute" - вычисление, "open/close" - открытие/закрытие, и т.д.  Также в паре следует использовать следующие имена: add/remove, create/destroy, start/stop, insert/delete, increment/decrement, old/new, begin/end, first/last, up/down, min/max, next/previous, old/new, open/close, show/hide. Т.е. если вы одну функцию назвали AddTitle(), то противоположную по действию надо назвать не DestroyTitle() или DeleteTitle(), а RemoveTitle().

9. Перед именами переменных, представляющих количество чего-то, ставьте префикс "n": nColors, nItems. Переменные, обозначающие порядковый номер чего-то, дополняйте суффиком "No": RecordNo, LineNo.

10. Не злоупотребляйте сокращениями. Согласитесь, что, например, смысл GetListAverage() гораздо легче понять, чем GetLstAvg() (ведь это, в принципе, может обозначать и GetLastAvenger() ;-).

11. Избегайте логических переменных, обозначающих отрицание. Found, а не notFound; Good, а не notGood. Вообще, хорошим стилем считается дополнять логические переменные префиксом "is": isFound, isGood. Это же относится к функциям, возвращающим значение true/false, напр. IsKindOf().

12. Константы из типов-перечислений (enum) должны содержать имя типа. COLOR_BLUE, а не BLUE; FILE_ERROR_ALREADY_EXISTS, а не ALREADY_EXISTS.

13. Всегда приводите типы к нужным явно, не полагаясь на автоматику.

14. Переменные, связанные между собой по смыслу, можно объявлять одной строкой:
int x,y,z;
Record first,last, next, previous;
Никак не связанные переменные лучше объявлять на разных строках, даже если они одного типа.

15. В пустых циклах хорошо явно прописывать continue. Этим вы показываете, что оставили цикл пустым нарочно, а не по ошибке. Пример: while ( *p++ = *q++ ) continue;

Ну, хватит пожалуй. Если кто-то особенно заинтересовался этим вопросом, он может посмотреть более чем 70 подобных правил в Geosoft's C++ Programming Style Guidelines.
Должен заметить, что далеко не со всеми положениями этого документа я согласен. Например, я не считаю нужным обязательно начинать имена функций с маленькой буквы, - действительно важно различать переменные и типы, а функцию от типа отличить гораздо легче. Или еще, например, правило всем private-членам классов давать суффикс "_" - ну вот не нравится и все тут... Я здесь привел самые, на мой взгляд, нужные и, прошу прощения за тавтологию, "правильные" правила; те, которые встречаются практически во всех документах такого типа.
Так что дам еще один, последний, совет: подходите ко всем правилам, необходимость которых для вас не очевидна, с некоторой толикой здравого смысла.

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

|Q Как в VC++ 6.0 можно сделать окно, которое не будет видно на Taskbar'e? - Kirill

|A Самый простой способ - это создать основное окно с расширеным стилем окна WS_EX_TOOLWINDOW:

hWnd = CreateWindowEx(WS_EX_TOOLWINDOW, szWindowClass, szTitle, < BR >         WS_OVERLAPPEDWINDOW,0,0,100,100,NULL, 0, hInstance, NULL);

При использовании MFC следует перекрыть метод PreCreateWindow ():

      BOOL CMainFrame::PreCreateWindow(CREATESTRUCT&
  cs)
{
if(
  !CMDIFrameWnd::PreCreateWindow(cs) ) return
  FALSE;

cs.dwExStyle=WS_EX_TOOLWINDOW;

return TRUE;
}

Но такое решение не всегда приемлемо - у созданного таким образом окна на тайтлбаре может находится только кнопка закрытия, и его заголовок отличается от заголовков других окон (он меньше). Для того, чтобы исправить эти недостатки, сначала создаем невидимое окно со стилем WS_EX_TOOLWINDOW, а затем дочернее окно, которое будет выполнять роль основного окна приложения. Это будет выглядеть следующим образом:

//...
HWND hWnd1,hWnd2;
hInst = hInstance;
hWnd1 = CreateWindowEx(WS_EX_TOOLWINDOW , szWindowClass1, szTitle, 0, 0, 0, 100, 100, NULL, 0, hInstance, NULL); // cоздаем невидимое окно

// создаем окно, которое будет основным; указываем hWnd1 в кач.родителя:
hWnd2 = CreateWindowEx(0, szWindowClass2, szTitle, WS_OVERLAPPEDWINDOW, 0, 0, 100, 100, hWnd1, 0, hInstance, NULL);

ShowWindow(hWnd1, FALSE);    // скрываем 1-ое окно
UpdateWindow(hWnd1);

ShowWindow(hWnd2, nCmdShow); // делаем дочернее окно видимым
UpdateWindow(hWnd2);

- Bad Sector

|Небольшое дополнение:Если вам нужно убирать кнопку с таскбара только тогда, когда ваше приложение минимизировано (например, чтобы реализовать функцию "минимизировать в системный трей"), то все становится гораздо проще. Достаточно в обработчике OnSysCommand поставить реакцию на минимизацию окна ( т.е. когда параметр nID равен SC_MINIMIZE вызывать ShowWindow(hWnd, SW_HIDE) ). А при получении соответствующего сообщения от иконки в трее, не забывать восстановить окно. (про работу с системным треем см. выпуск No.11)

/ / / В ПОИСКАХ ИСТИНЫ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Q| В Visual C++ 6.0 создаётся ImageList с помощью ImageList_LoadImage. Потом две загруженные картинки рисуются в окошке - сначала одна, потом поверх неё другая (используется маска) - функция ImageList_Draw. Проблема в том, что рисуется только в 16 стандартных цветах. Картинка 24-битная. Пробовал и с 256 и 16-цветными, с использованием палитры - эффект тот же. Если не сложно, подскажи, как её нарисовать в 16M цвете (использую только API, без MFC)? - Дрон
/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Всего вам доброго и не скучайте !

©Алекс Jenter mailto:jenter@mail.ru
Красноярск, 2000.


No.16

Выпуск No. 16 от 23 сентября 2000 г.

Здравствуйте!

Э-э-х, непростая у нас, программистов, профессия! Иногда просто откровенно завидуешь медикам, лингвистам, архитекторам, ученым и всем остальным, которые отучились в институте - и могут спать спокойно: если что-то принципиально новое и появляется в их области, то не слишком часто и точно не слишком помногу.
У нас же в одночасье, иногда по хотению и велению только ОДНОЙ фирмы, все может перевернуться с головы на ноги (чаще наоборот), и вот ты обнаруживаешь, что никокой ты не специалист, - тебе надо еще учиться, учиться и учиться (не помню кто сказал ;)
Когда смотришь объявления о работе для программистов, частенько кружится голова от всяких аббревиатур и названий на английском языке - MFC, ATL, COM, ASP, PHP, SQL, HTML, DHTML, XML, UML, VB, VBA, VBScript, C++, Java, JavaScript, Perl, CGI, TCP/IP, OpenGL, DirectX и пр. и пр. Кстати, я в парочке объявлений уже заметил C# , хотя он еще даже не вышел!
И вот видишь в объявлении совершенно непредсказуемую комбинацию из четырех-пяти вышеперечисленных названий, и думаешь - неужели есть кто-то, кто это все знает? Да еще имеет 2-3 года опыта работы с этим?
А вот в том-то и дело, что таких людей не слишком много. Есть один практический совет: если вы хотя бы на 60% удовлетворяете требованиям работодателя, посылайте резюме!

Мне могут возразить, что мол старые технологии остаются востребованными всегда, наряду с новыми. Я скажу одно: когда вы последний раз видели, чтобы требовался программист под MS-DOS? А я помню время, когда под Windows (тогда еще 3.1) программировали считанные единицы (и то все считали это недостойным занятием), а большинство работало именно в DOS.

Утешить может только одно: постигая что-то конкретное, мы также постигаем общий принцип, по которому это конкретное сделано. А вот знание общих принципов, господа - действительно помогает. Уже зная пару-тройку языков, новый язык программирования вполне реально изучить за две недели, за месяц - писать на нем сносные программы, за полгода - стать профессионалом. Только через полгода обязательно появится еще что-нибудь новое...;) Поэтому люди сейчас поступают умнее: они осваивают новое еще ДО того, как это новое появится. Нет, они вовсе не путешествуют во времени...

/ / / / ОБРАТНАЯ СВЯЗЬ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Статья про WTL в 14-ом выпуске не осталась незамеченной. Некоторые заитересовались этой темой, некоторые захотели высказать свое мнение. Вот одно из таких писем:

Добрый день (вечер, ночь, утро) Алекс и подписчики "Программирование на Visual С++" (в случае, если мое письмо опубликуют в рассылке).
После прочтения выпуска No.14 у меня появилась пара мыслей по поводу будущего MFC и WTL. Точнее, мысли эти у меня есть давно (месяца два-три), возможно, вы об этом тоже знаете. То, что MFC умирает - это факт. С самого начала она была мертворожденной. Постоянные баги, внутренняя сложность, обилие недокументированных внутренних структур и функций - все это не делает библиотеку хорошей. Я уже не говорю о ее монстрообразности. Да, с ней удобно работать. Но стоят ли проблемы удобства? Можно говорить очень долго о достоинствах и недостатках MFC, но факт остается фактом: будущего у нее нет.
Что касается WTL, то здесь тоже все непросто. Точнее, если верить одному из мэнэджеров Майкрософт (к сожалению, не помню кто), то все очень просто: у WTL будущего тоже нет. В одном из выступлений он заметил, что WTL не будет поддерживаться Майкрософт. Правда, не исключено, что WTL получит свое развитие.
Ну и последнее. Я думаю, все слышали о проекте Майкрософт .NET. Я занимаюсь им в свободное время и могу сказать одно: это действительно отличная вещь. Пока еще сырая, нет даже беты, но уже в следующем обновлении MSDN (октябрь) должна появиться Visual Studio 7 Beta (или Visual Studio.Net). Не хочу рассказывать о .NET, не для маленького письма тема, к тому же, я не настолько хороший лектор, как Jeffrey Richter и Don Box. Поэтому всем, кто интересуется, могу посоветовать пару ссылок: http://www.msdn.microsoft.com/net/ , http://www.andymcm.com/ и замечательный список рассылки DOTNET на http://discuss.develop.com.

- Alex Ivanoff

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

Q1| В Visual C++ 6.0 создаётся ImageList с помощью ImageList_LoadImage. Потом две загруженные картинки рисуются в окошке - сначала одна, потом поверх неё другая (используется маска) - функция ImageList_Draw. Проблема в том, что рисуется только в 16 стандартных цветах. Картинка 24-битная. Пробовал и с 256 и 16-цветными, с использованием палитры - эффект тот же. Если не сложно, подскажи, как её нарисовать в 16M цвете (использую только API, без MFC)? - Дрон

|A Насколько я понимаю, в функцию ImageList_LoadImage следует передать флаг LR_CREATEDIBSECTION, чтобы избежать преобразований цветов в изображении.
Например:

 ...
HIMAGELIST hImageList;
...
hImageList = ImageList_LoadImage( hInstance, MAKEINTRESOURCE(IDB_BITMAP1), 100, 0, CLR_NONE, IMAGE_BITMAP, LR_CREATEDIBSECTION);
...

- Alexander Shargin

Такой же ответ на этот вопрос прислал David Potashinsky. Большое спасибо всем, кто откликнулся.

У меня выдалось немного свободного времени, поэтому следующий вопрос я не стал помещать в рубрику "В поисках истины", а ответил на него сам:

Q2| Меня попросили сделать страничку свойств (CPropertySheet), которая является главным окном приложения, минимизируемой. Означенная просьба неожиданно оказалась не столь простой как кажется на первый взгляд. Добавить собственно значок минимизации - нет проблем: в CPropertySheet::OnInitDialog добавляем ModifyStyle(0, WS_MINIMIZEBOX). Одно плохо - не работает он. - Олег

|A Сначала скажу, что эта идея - property sheet в качестве главного окна приложения - довольно неудачная. Сейчас объясню, почему. С точки зрения Windows Ваше приложение является не окном в полном смысле слова, а диалогом, причем таким диалогом, кнопка которого на панель задач не выводится. Т.о. при минимизации произойдет вовсе не то, что Вы ожидали - он минимизируется а-ля windows 3.1 (или как дочернее окно MDI) - в левый нижний угол.
Можно конечно ухитриться и сделать так, чтобы показывалась кнопка на таскбаре (см. предыдущий выпуск), но приемлемый, на мой взгляд, выход в данной ситуации - минимизировать в системный трей.

Теперь - что надо сделать, чтобы кнопка минимизации заработала:
При нажатии на эту кнопку для такого окна генерации события WM_SYSCOMMAND не происходит, и обрабатывать его нет смысла. Поэтому в класс-наследник CPropertySheet нужно добавить обработчик события WM_NCLBUTTONDOWN (non-client left button down, это событие происходит, когда нажимается левая кнопка мыши в неклиентской области окна, а значок минимизации как раз и находится в этой области):

void CMinSheet::OnNcLButtonDown(UINT nHitTest, CPoint point)
{
   // TODO: Add your message handler code here and/or call default
   CPropertySheet::OnNcLButtonDown(nHitTest, point);

   if (nHitTest == HTMINBUTTON)
   {
      IsIconic()? ShowWindow(SW_RESTORE): ShowWindow(SW_MINIMIZE);
   }
}

Заметьте, что в свернутом состоянии кнопка минимизации (minimize) становится кнопкой восстановления (restore).
Повторю, в этом случае минимизироваться диалог будет в левый нижний угол. Лучше всего вместо минимизации его прятать [ ShowWindow(SW_HIDE); ] и выводить иконку в трее (как это делать см. выпуск рассылки No.11).

Если кто-то из вас, уважаемые подписчики, не согласен в чем-то с этим ответом, или есть какие-нибудь дополнения - обязательно напишите.

Подробно про класс CPropertySheet вы сможете прочитать в следующем выпуске.

/ / / В ПОИСКАХ ИСТИНЫ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Q| Возникла вот такая задачка. Имеется некоторое разбиение SDI на несколько view при помощи сплиттеров (A). Как его изменить не убивая окна (на B или C)? - Nikita Zeemin

   +--+----+     +--+----+    +--+----+
   |  |    |     |  |    |    +  +____+
   +--+----+     +--+    |    +  +    +
   |       |     |  |    |    +  +    +
 A +-------+   B +--+----+  C +--+----+

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

До встречи!

©Алекс Jenter mailto:jenter@mail.ru
Красноярск, 2000.


No.17

Выпуск No. 17 от 29 сентября 2000 г.

Всем привет!

До меня дошли сведения, что предыдущий, 16-тый, выпуск дошел почему-то не до всех подписчиков. То ли из-за глюков на ГорКоте, то ли из-за гиперактивности магнитных бурь ... ;)
Повторно высылать я его, пожалуй, не стану, лучше дам вам на него прямую ссылку. Тем, кто его еще не видел, рекомендую прочитать,так как данный выпуск связан с предыдущим.

/ / / / MFC / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Итак, наконец-то мы добрались до темы создания окон свойств, которые в MFC реализуются с помощью, как вы уже догадались, класса CPropertySheet.

Работа с окнами свойств : использование класса CPropertySheet

Как известно, практически во всех более-менее серъезных программах есть диалоговые окна настройки параметров, или опций, приложения. Такие окна получили название окон свойств. Чтобы увидеть одно из таких окон, достаточно выбрать Tools|Options в Visual C++ IDE.

Задача создания окон свойств стоит практически перед каждым разработчиком, именно поэтому в MFC решение этой проблемы в некоторой степени автоматизировано с помощью класса CPropertySheet. В результате его применения вы получаете готовое диалоговое окно с набором закладок и ;некоторым количеством стандартных кнопок - OK, Cancel, и т.д. Закладки здесь - это объекты типа CPropertyPage. Этот класс, кажется, уже фигурировал в одном из выпусков. Никогда не путайте CPropertySheet и CPropertyPage: помните, что первый (CPropertySheet) СОДЕРЖИТ вторые (CPropertyPage) так же, как книга содержит страницы.

Так, с этим разобрались, идем дальше. Как пользоваться классом CPropertySheet? Очень несложно, вы в этом сами сейчас убедитесь.

Для каждой закладки нужно создать диалоговый ресурс (не обязательно со стилем child), куда вы помещаете все содержимое соответствующей страницы (также, как и при работе с CTabCtrl). Например, IDD_PROPPAGE1 и IDD_PROPPAGE2. Можно сразу заполнить поле Caption в диалогах, чтобы потом заголовки закладок сформировались автоматически.

В проект добавляется класс-наследник от CPropertySheet, пускай он называется CMyPropSheet.
Для того, чтобы можно было работать с контролами на закладках, добавляется отдельный класс для каждой страницы-закладки (наследованный от CPropertyPage). Например, для двух закладок это будут классы CPropPage1 и CPropPage2 (эти классы добавьте дабл-кликнув на поверхности соответствующего диалога и выбрав "Create a new class", затем в поле "Base class" выберите CPropertyPage в качестве класса-родителя). В эти классы нужно поместить члены, связанные с контролами, расположенными на странице. Например, если у нас на первой странице (IDD_PROPPAGE1) есть Edit Box, добавляем в класс CPropPage1 переменную m_strEdit класса СString, доступ - public. Пусть на второй странице у нас Check Box, значит в класс CPropPage2 записываем член m_isChecked типа BOOL, и т.д. Использование типа доступа public к этим полям в данном случае оправданно, т.к. избавляет в дальнейшем от многих хлопот. И не забывайте, эти члены класса должны быть связаны с соответствующими контролами на закладке.

Теперь в файл mypropsheet.h (где объявлен класс CMyPropSheet) пишем:

#include "proppage1.h" // делаем классы страниц видимыми
#include "proppage2.h"
...
class CMyPropSheet: public CPropertySheet
{
 ...
 protected:
  CPropPage1 page1; // первая страница
  CPropPage2 page2; // вторая страница
 ...
}

Чтобы добавить страницы к окну свойств, необходимо в каждый из конструкторов СMyPropSheet вставить по две следующие строчки:

AddPage(&page1);
AddPage(&page2);

Таким образом, мы сделали страницы-закладки частью нашего окна свойств. Настал момент теперь решить, где вы собираетесь хранить настройки вашего приложения. Я это обычно делаю в классе главного окна, по причине легкости доступа, но ничто не мешает вам хранить их там, где удобнее, причем не обязательно все вместе. Неплохой вариант - хранить их в классе приложения, даже с некоторой точки зрения он более логичный.
Но предположим, вы решили хранить их в классе главного окна, а чтобы они не перемешивались с другими полями класса, объединить их в структуру:

...
class CMainFrame: public CFrameWnd
{
 ...
 public:
  struct Options
  {
     CString str;
     BOOL val;
  } options;
 ...
}

В конструкторе CMainFrame поля структуры options установите в начальные значения. Их можно задать жестко, но обычно сохраненные ранее значения загружаются из файла или реестра , иначе вы быстро доведете пользователя вашей программы до белого каления, заставляя его менять параметры после каждого запуска.
Теперь вставьте обработчик события, возникновение которого должно приводить к выводу на экран вашего окна свойств (например, выбор пункта меню "Сервис|Параметры..."). В обработчике вы устанавливаете параметры, после чего выводите окно свойств. Если пользователь нажал "OK", то после закрытия окна свойств нужно обновить структуру options:

#include "mypropsheet.h"
...
void CMainFrame::OnToolsOptions()
{
  CMyPropSheet ps("Параметры приложения", this, 0);

  ps.page1.m_strEdit = options.str;  // настраиваем закладки
  ps.page2.m_isChecked = options.val;  // соответственно текущим параметрам

  if ( ps.DoModal() == IDOK ) // если пользователь нажал OK
  {
   options.str = ps.page1.m_strEdit; // сохраняем параметры
   options.val = ps.page2.m_isChecked;
  }
}

Вот и все, что касается элементарного использования класса CPropertySheet для создания окна свойств. Как видите, работать с ним довольно просто. В одном из следующих выпусков я расскажу вам о задействовании кнопки "Применить" ("Apply"), о нетривиальном использовании этого класса для создания мастеров (wizards), а также о расширенном классе CPropertySheetEx.

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

Q| Возникла вот такая задачка. Имеется некоторое разбиение SDI на несколько view при помощи сплиттеров (A). Как его изменить не убивая окна (на B или C)? - Nikita Zeemin

   +--+----+     +--+----+    +--+----+
   |  |    |     |  |    |    +  +____+
   +--+----+     +--+    |    +  +    +
   |       |     |  |    |    +  +    +
 A +-------+   B +--+----+  C +--+----+
|A Для создания окна сплиттера в MFC служит класс CSplitterWnd. Этот класс предоставляет функции для создания вида (CreateView) и удаления вида DeleteView) в заданной панели, но не предоставляет функции, которая позволила бы перенести вид из одной панели в другую. Чтобы проделать это вручную, нужно понимать, каким образом связаны объект класса CSplitterWnd и объекты дочерних видов CView.

CSplitterWnd может иметь не более 16 панелей по горизонтали и столько же по вертикали. Таким образом, он может сожержать не более 256 панелей. Каждой панели соответствует уникальный идентификатор, который и назначается тому виду, который в этой панели находится. Отображение координат панели на её идентификатор выполняет функция int CSplitterWnd::IdFromRowCol( int row, int col );
На самом деле после целой серии ASSERT'ов она просто возвращает значение
AFX_IDW_PANE_FIRST + row * 16 + col,
где AFX_IDW_PANE_FIRST - константа, объявленная в MFC.
Это подсказывает простой способ перемещения вида из одной панели в другую: нужно всего лишь подменить его идентификатор, после чего вызвать CSplitterWnd::RecalcLayout для обновления содержимого сплиттера. Если изначально вид не являлся дочерним окном сплиттера и требуется поместить его в одну из панелей, то необходимо также поменять ему родителя с помощью функции CWnd::SetParent. Таким образом функция, вставляющая вид в заданную панель, может выглядеть примерно так:

class CMySplitter : public CSplitterWnd
{
 ...
 void InsertView(int nRow, int nCol, CWnd *pView)
 {
  pView->SetParent(this);
  pView->SetDlgCtrlID(IdFromRowCol(nRow, nCol));
 }
 ...
}

Аналогичным образом вид переносится "в юрисдикцию" главного окна приложения, порождённого от CFrameWnd:

class CMyFrame : public CFrameWnd
{
 ...
 void InsertView(CWnd *pView)
 {
  pView->SetParent(this);
  pView->SetDlgCtrlID(AFX_IDW_PANE_FIRST);
 }
 ...
}

Имея в руках эти две функции, можно без труда решить поставленную задачу, располагая виды как на рис. А, B, C или любым другим способом. Ещё раз замечу, что после всех перемещений необходимо вызывать CSplitterWnd::RecalcLayout и CFrameWnd::RecalcLayout.

- Alexander Shargin

/ / / / ОБРАТНАЯ СВЯЗЬ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Alexander Shargin, воистину герой сегодняшнего выпуска, в письме с ответом на предыдущий вопрос также недоумевал по поводу вопроса прошлого выпуска, где требовалось минимизировать CPropertySheet:

...Честно говоря, я не понимаю, о чём идёт речь. Я буду очень вам признателен, если вы объясните мне, в чём проблема. Дело в том, что я создал визардом приложение на базе диалога, а затем просто заменил диалог на объект класса CMySheet, порождённого от CPropertySheet. После чего добавил пару вкладок (типа CPropertyPage) и вызов ModifyStyle(0, WS_MINIMIZEBOX) в обработчике OnCreate. В результате этих несложных операций получилось приложение, главное окно которого без проблем сворачивается на панель задач.

Я посмотрел приаттаченный им проект и убедился, что Александр совершенно прав. После чего я сам проделал то же самое с новым проектом, и получил такой же результат. Итак, вся загвоздка была в том, что ModifyStyle нужно было вызывать не из OnInitDialog, а из OnCreate!

После этого я задумался, откуда же у класса CPropertySheet вообще есть метод OnInitDialog, ведь сам класс является прямым наследником CWnd. Оказалось, что этот метод, наряду с DoModal, был добавлен туда искусственно, чтобы обращение с классом напоминало обращение с CDialog. Не знаю, почему бы Microsoft просто не сделать CPropertySheet наследником CDialog, но наверное у них были свои причины (хотя здесь можно и посомневаться ;)

Я переслал письмо Александра человеку, задавшему вопрос, и получил от него положительный ответ - у него тоже все заработало.

Вот, оказывается, как просто открывался ларчик! Не надо было перехватывать WM_NCLBUTTONDOWN, не нужно было делать callback функцию... (решение автора вопроса)...
И еще - насчет минимизации в левый нижний угол - видимо это был частный случай поведения, вызванный моими манипуляциями со стилями ;-)

Напоследок хочу процитировать Win32 Q&A из MSDN, чтобы абсолютно точно уяснить для всех

Как Windows определяет, нужно ли выводить кнопку приложения на панель задач

Правила эти довольно просты, хотя и не очень хорошо документированы. Когда вы создаете окно, Windows проверяет его расширенный стиль. Если установлен стиль WS_EX_APPWINDOW (определенный как 0x00040000), на панель задач выводится кнопка окна. Если же установлен стиль WS_EX_TOOLWINDOW ( 0x00000080 ), то кнопка не выводится. Не следует создавать окна, где установлены оба эти стиля.
Можно создать окно, не имеющее ни один из этих стилей. Тогда кнопка выводится только в том случае, если у окна нет родителей.
И последнее замечание: прежде чем тестировать что-либо из вышеописанного, панель задач проверяет, установлен ли стандартный стиль видимости WS_VISIBLE. Если нет, то окно спрятано, и показывать кнопку нет никакого смысла. Стили WS_EX_APPWINDOW, WS_EX_TOOLWINDOW и информация о принадлежности окна проверяются ТОЛЬКО при установленном WS_VISIBLE. - Jeffrey Richter

/ / / В ПОИСКАХ ИСТИНЫ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Q| У меня программа с использованием MFC и Doc/View. Я вставил RichEditCtrl во вью. (2-ой версии). Установил шрифт с помощью сообщения SetCharFormat. Внимание, вопрос: почему если я ввожу текст с клавиатуры и использую ReplaceText функцию (не сообщение!) фонты различные? Вроде это сообшение не менялось у второй версии. Заранее спасибо за ответ. - Игорь

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Это все на сегодня. Пока!

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


No.18

Выпуск No. 18 от 7 октября 2000 г.

Приветствую вас!

После публикации статьи про CPropertySheet меня обвинили в чрезмерной ориентированности на начинающих программистов (на самом деле было использовано другое слово). Так что материал этого выпуска как раз для чуть-чуть более "продвинутых".

И еще, хочу чтобы вы приняли к сведению: я отвечаю НЕ НА ВСЕ приходящие письма. Когда вы присылаете мне вопрос, не нужно рассчитывать на то, что я обязательно на него отвечу или помещу в рубрику "В поисках истины".

В обязательном порядке это делается только для тех читателей, кто до этого сам присылал статьи или ответы на вопросы (и которые были впоследствии опубликованы в рассылке).

/ / / / MFC / / / / / / / / / / / / / / / / / / / / / / / / / / /

Сегодня я хочу представить вашему вниманию статью - перевод из MSJ C++ Q&A, которую прислал Илья Простакишин.

Решение проблемы с OnIdle в dialog-based приложениях

Проблема в том, что OnIdle работает нормально только в приложениях document/view, что не скажешь о приложениях dialog-based. Функция CApp::InitInstance вызывает dlg.DoModal, которая в свою очередь вызывает CWnd::RunModalLoop, а та никогда не обращается к OnIdle. Кажется, что можно производить фоновую обработку посредством WM_ENTERIDLE, но это сообщение направляется только родительскому окну диалога, которого в нашем случае просто не существует. Как решить эту проблему?

Модальные диалоги на самом деле лишь имитируются в MFC. Когда вы вызываете CDialog::DoModal, MFC не вызывает ::DialogBox, как можно было ожидать; вместо этого вызывается ::CreateDialogIndirect, затем модальное поведение имитируется путем блокировки родительского окна и запуска своего собственного цикла обработки сообщений. По существу, тоже самое делает функция ::DialogBox. Тогда зачем изобретать велосипед? А дело в том, что теперь MFC имеет свой собственный цикл, в то время как раньше он "прятался" внутри функции API ::DialogBox. Это позволяет MFC обрабатывать сообщения посредством обычных потоков MFC (CWinThread::PumpMessage), что и делается с другими типами окон. В результате вы можете переопределять CWnd::PreTranslateMessage для модальных диалогов - например, для реализации "горячих" клавиш. Ранние версии MFC позволяли реализовывать свою собственную функцию PreTranslateMessage для модального диалога. Но толку от этого было мало, ведь она все равно никогда не вызывалась, т.к. CDialogDoModal напрямую обращалась к ::DialogBox. При этом управление в программу не возвращалось, пока один из обработчиков сообщений вашего диалога не вызывал EndDialog. По этой же причине была невозможна обработка интервала ожидания.

Вместо этого Windows имеет свой собсвенный механизм, WM_ENTERIDLE, предназначенный для обработки интервала ожидания в модальных диалогах. После обработки одного или нескольких сообщений, если в очереди больше ничего нет, Windows автоматически посылает WM_ENTERIDLE окну-владельцу модального диалога или меню. Работает это только в модальных диалогах. Поскольку MFC теперь использует немодальные диалоги в качестве модальных, WM_ENTERIDLE посылается библиотекой "вручную", чтобы самостоятельно имитировать модальные диалоги, но, опять же, только если имеется родительское окно.

Хорошо, если все сообщения модального диалога проходят через стандартную очередь, почему не вызывается CWnd::OnIdle, как часть этого процесса? Проблема в том, что функция CWnd::RunModalLoop вызывает CWinThread::PumpMessage, однако OnIdle вызывается только внутри CWinThread::Run. MFC вызывает CWinThread::Run для запуска вашего приложения после вызова функции InitInstance. В сокращенном виде функция CWinThread::Run выглядит так:

// (from THRDCORE.CPP)
int  CWinThread::Run()
{
//
for doing idle cycle
BOOL bIdle = TRUE;
LONG lIdleCount = 0;
for
 (;;)
{
while
(bIdle && !::PeekMessage(...))
{
// call OnIdle while in bIdle
state
if
(!OnIdle(lIdleCount++))    // assume "no idle" state
bIdle =
FALSE;
}
// Get/Translate/Dispatch the message
// (calls
CWinThread::PumpMessage)
...
}
}

Я убрал все лишнее, чтобы заострить внимание на том, как реализована обработка интервала ожидания. Если соообщений не поступает, MFC вызывает CWinThread::OnIdle, каждый раз увеличивая аргумент-счетчик. Вы можете использовать этот счетчик для установки приоритета различиных обработчиков интервала ожидания. Например, можно выполнять форматирование (диска C:, например :)), когда счетчик ожидания равен 1, затем обновлять показания часов при счетчике, равном 2 и т.п. Если OnIdle возвращает FALSE, то MFC прекращает его вызывать и ждет пока ваш поток получит какое-нибудь сообщение, просле чего обработчик будет вызываться снова.

Обратите внимание, что модальный диалог никогда не будет выполнять этот код, поскольку CWnd::RunModalLoop вызывает CWinThread::PumpMessage сразу из своего собственного цикла обработки сообщений. Он не вызывает CWinThread::Run и, следовательно, никогда не обращается к CWinThread::OnIdle. По-видимому, так было задумано разработчиками. Очевидно, что опасно вызывать OnIdle внутри модального диалога, поскольку многие обработчики сообщений создают временные объекты CWnd, которые, вполне возможно, будут существовать все время, пока существует диалог. Частью процесса обработки OnIdle является освобождение временных карт дескрипторов (handle).

Не могу удержаться от упоминания о том, что механизм временной/постоянной карты дескрипторов (handle map), используемый для связывания дескрипторов HWND с классами CWnd - это один из самых потенциально опасных элементов каркаса приложения, даже более опасный, чем карты сообщений. Проблема "временной карты" непрерывно преследует программистов, особенно в многопотоковых приложениях, делая их трудными для написания в MFC.

Итак, каким же образом обрабатывать период ожидания в приложениях, основанных на диалоге, которые не имеют родительского окна? К счастью, это довольно просто. Разработчики MFC предусмотрели возможность перехвата WM_KICKIDLE. RunModalLoop посылает это частное сообщение MFC (вы не найдете его описания в стандартной документации по Win32 API) все время, пока в очереди диалога нет сообщений, так же как CWinThread::Run вызывает OnIdle. RunModalLoop также поддерживает счетчик и увеличивает его для вас. В результате, WM_KICKIDLE является диалоговым эквивалентом OnIdle. Историческая справка: в ранних версиях MFC была реализована подмена модальных диалогов немодальными в комплексе с WM_KICKIDLE в страницах свойств (property sheets). Видимо, эта схема настоько понравилась, что в дальнейшем все немодальные диалоги стали маскироваться под модальные.

Маленькое замечание: у вас может появиться искушение вызвать функцию OnIdle основного приложения. Вот так, например:

LRESULT CMyDlg::OnKickIdle(WPARAM, LPARAM lCount)
{
  return AfxGetApp()->OnIdle(lCount);
}

Разработчики MFC утверждают, что это опасно, в связи с проблемой пресловутой временной карты. Лучше всего реализовывать обработку интервала ожидания внутри OnKickIdle. Если хотите, то можно объединить общие команды обработки в отдельную функцию, которую и вызывать из CApp::OnIdle и CMyDlg::OnKickIdle.

Раз уж здесь был заведен разговор о предмете обработки интервала ожидания, то не лишним будет упомянуть о следующем - не все программисты знают, что есть также функции OnIdle для классов CDocTemplate и CDocument. Если вы хотите реализовать обработчик периода ожидания для документа или шаблона документа, то все что нужно сделать - это определить одну из этих функций.

- Paul DiLascia (72400.2702@compuserve.com)
Copyright(C) 1995 by Miller Freeman, Inc.
Перевод: Илья Простакишин (iliya@yes.ru)

Если у вас есть материал (на русском языке) по теме, которая, как вы считаете, будет интересна подписчикам рассылки, не стесняйтесь и присылайте.

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

Q| У меня программа с использованием MFC и Doc/View. Я вставил RichEditCtrl во вью. (2-ой версии). Установил шрифт с помощью сообщения SetCharFormat. Внимание, вопрос: почему если я ввожу текст с клавиатуры и использую ReplaceText функцию (не сообщение!) фонты различные? Вроде это сообшение не менялось у второй версии. Заранее спасибо за ответ. - Игорь

|A Думаю, всё дело в режиме IMF_AUTOFONT, который по умолчанию устанавливается для rich edit'а 2-й версии (в 1-й этого режима просто не было). В этом режиме rich edit автоматически переключает язык и фонт, когда пользователь переключает раскладку клавиатуры (в текст rtf вставляется управляющая последовательность "\langXXXX\fX"). Поэтому если язык, установленный в rich edit'е по умолчанию, отличается от выбранного при запуске программы, фонт подменяется как только кто-то начинает набирать текст с клавиатуры, что и приводит к описанному эффекту.

Для решения проблемы следует попробовать отключить режим IMF_AUTOFONT. Выглядит это так (hEdit - дескриптор rich edit'а):

::SendMessage( hEdit,
               EM_SETLANGOPTIONS, 0,
               ::SendMessage(hEdit, EM_GETLANGOPTIONS, 0, 0) & ~IMF_AUTOFONT);

- Alexander Shargin

 Огромное спасибо Александру за ответ (уже, кстати, третий по счету).

Напоминаю, что в будущем вопросы от Ильи, Александра и всех тех, чьи ответы или материалы были опубликованы, будут рассматриваться вне очереди.

/ / / В ПОИСКАХ ИСТИНЫ / / / / / / / / / / / / / / /

Q| Как просканировать LAN на предмет создания поименного списка машин, чтобы затем можно было изпользовать результат в ListBox'e? Пробовал использовать для этой цели SHBrowseForFolder( ) и связанные ф-ции с установленным флагом CIDL_NETWORK, но открывающееся окно для выбора узла и необходимость "раскрывать плюсики" в локальных группах меня не устраивает. Если можно, в API без MFC. - DevXarT

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

До встречи!

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


No.19

Выпуск No. 19 от 15 октября 2000 г.

Приветствую всех!

/ / / / WinAPI & MFC / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Тонкая настройка панели инструментов
 Задание внешнего вида кнопок и размещение отличных от кнопок элементов

Панель инструментов (Toolbar) cейчас является, пожалуй, одной из обязательных частей любого профессионально сделанного приложения. И тем более становится обидно, что используя стандартные заготовки, ничего, кроме маленьких кнопочек поместить на эту панель нельзя.

Но что делать, если внешний вид приложений MS Office или VC IDE не дает вам спокойно спать? Если вам просто необходимо идти в ногу с конкурирующими программами, где с этим как раз все в порядке? Если вы решили, что размещение дополнительных элементов управления на панели инструментов сразу сделает интерфейс гораздо понятнее и удобнее ?..

Тогда нужно просто взять и сделать это. Да, для этого придется приложить некоторые усилия. Так что любители "рисовать приложения" могут спокойно об этом забыть, а еще лучше - перейти на С++Builder или Delphi, где их способности к рисованию смогут реализоваться в полной мере. (Ладно, не обижайтесь.) Самое большое преимущество Visual C++ в том, что при желании с его помощью можно сделать практически ВСЕ, ЧТО УГОДНО. Главное, знать как. Не пугайтесь - от вас потребуется совсем немного.

Но для начала давайте выясним, можно ли просто изменить стандартный вид тулбара (так для краткости я буду именовать панель инструментов). Например, сделать тулбар а'ля WinZip - большие иконки, с подписями...

Итак, какие настройки нам доступны практически сразу же? - Конечно же, стили. Оперируя стилями, мы тоже можем повлиять на внешний вид панели. Вот самые интересные из них:

ССS_TOP и ССS_BOTTOM - задают размещение тулбара - вверху или внизу окна. (CСS_TOP используется по умолчанию.)
TBSTYLE_FLAT - Делает кнопки плоскими. (Используется по умолчанию.)
TBSTYLE_LIST - Используется для вывода текста справа от кнопки. (Помните пункт "выборочно текст справа" в IE4 и Outlook?)

Остальные стили вы можете, в принципе, посмотреть сами - они задают некоторые другие параметры. Но нам сейчас нужно другое: сделать кнопки большими и с подписями. Выполнить это можно с помощью членов классов CToolBar и CToolBarCtrl, а если вы работаете не в MFC, то придется работать с сообщениями.
Вышеупомянутые классы MFC располагают богатым набором функций для всевозможной настройки тулбара. Функции CToolBarCtrl::SetButtonSize(), CToolBarCtrl::SetBitmapSize(), CToolBarCtrl::SetButtonWidth(), CToolBar::SetButtonText(), CToolBar::SetHeight() говорят сами за себя. Именно они понадобятся нам для реализации такого тулбара, как в WinZip. Без MFC это можно сделать, послав тулбару сообщения TB_SETBUTTONSIZE, TB_SETBITMAPSIZE, TB_SETBUTTONWIDTH, TB_ADDSTRING и др. Кстати, многие функции классов MFC делают фактически то же самое - просто посылают соответствующие сообщения.

Можете сами поиграться с различными настройками. Если бы я взялся здесь подробно описывать ВСЕ возможности, и как каждую реализовать, то эта статья растянулась бы минимум на дюжину выпусков. Цель данной публикации - дать вам общее представление, направление, так сказать. Я не думаю, что обязательно нужно все разжевывать и, как это часто происходит, сводить мысль к "нажмите туда, выберите то". Я хочу, чтобы вы учились думать сами.
Психологи приводят следующую статистику: когда человек просто прочитывает информацию, то запоминает не более 50% из прочитанного. Если еще и записывает - не более 70%.
Но если человеку информация досталась с некоторым трудом, если он ее нашел сам - то запоминается более 90%! Сделайте выводы.

Теперь пришла пора перейти к самому интересному вопросу, который я рассмотрю гораздо подробнее, а именно - как на тулбаре разместить что-либо, не являющееся кнопкой.
Я буду рассказывать как это делается в MFC, хотя понять, как это сделать в API после ознакомления с текстом не составит никакого труда.

Как обычно, если мы хотим заставить что-то стандартное работать нестандартно, нужно создавать класс-наследник. В нашем случае это будет наследник CToolBar, обозначим его CAdvBar. В нем вы сможете обрабатывать все события, поступающие от контролов, размещенных на тулбаре.

А теперь самая суть: как именно можно что-то поместить на тулбар. Дело в том, что существует возможность программно управлять шириной элементов-сепараторов. Таким образом, сепараторы становятся как бы "местами содержания" наших контролов.
В редакторе ресурсов отведите отдельную кнопку на тулбаре - в будущем вместо нее появится ваш элемент управления. Теперь для удобства определим следующие символические константы:

CONTROL_INDEX - порядковый номер кнопки-содержателя контрола;
CONTROL_WIDTH - ширина контрола;
CONTROL_HEIGHT - высота контрола.

Включите контрол в качестве члена в класс CAdvBar. Еще нужно создать функцию инициализации, которая будет подменять кнопку нашим контролом (конструктор или обработчик OnCreate в данном случае не подходят, т.к. вызываются слишком рано). Пускай, например, нам нужно поместить на тулбар элемент управления типа ComboBox. Тогда:

class CAdvBar : public CToolBar
{
   ...
   protected :
     CComboBox m_ComboBox;  // наш контрол

     void Initialize ( );  // ф-ция инициализации
   ...
}

Функция инициализации превращает нашу кнопку в сепаратор и устанавливает ему нужную ширину, после чего создает и позиционирует контрол на его место. Пример для комбобокса:

void CAdvBar::Initialize()
{
  CRect rc;

  SetHeight (CONTROL_HEIGHT + 8); // устанавливаем нужную толщину тулбара

  // превращаем кнопку в сепаратор нужных размеров
  // (IDC_COMBO - ID кнопки)
  SetButtonInfo (CONTROL_INDEX, IDC_COMBO, TBBS_SEPARATOR, CONTROL_WIDTH);

  GetItemRect (CONTROL_INDEX, rc);  // получаем координаты сепаратора

  // теперь создаем комбобокс.
  // не забывайте, что для этого контрола при создании указывается
  // его высота В РАЗВЕРНУТОМ ВИДЕ, именно поэтому
  // мы к ней прибавляем еще 100 пикселов.

  rc.bottom += 100;  < BR>
  m_ComboBox.Create( WS_CHILD | WS_VISIBLE | WS_TABSTOP |
                     WS_VSCROLL | CBS_AUTOHSCROLL | CBS_DROPDOWN,
                     rc, this, IDC_COMBO);

  // настраиваем контрол
  m_ComboBox.AddString( "Строка 1" );
  m_ComboBox.AddString( "Строка 2" );
  m_ComboBox.SetCurSel(0);
}

В данном случае еще могу порекомендовать в комбобоксе изменить шрифт на пропорциональный.

Вот, теперь все, что осталось сделать - в CMainFrame::OnCreate() после создания тулбара вызвать Initialize().

Взаимодейстиве с контролом, размещенным на тулбаре, происходит через соответствующий член класса и/или посредством сообщений.

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

Из ответов, содержащих одинаковое решение, я выбрал лучшие (с моей точки зрения). Большая просьба: не нужно присылать мне целые проекты и большие куски кода. Лучше объясните ваше решение словами.

Q| Как просканировать LAN на предмет создания поименного списка машин, чтобы затем можно было изпользовать результат в ListBox'e? Пробовал использовать для этой цели SHBrowseForFolder( ) и связанные ф-ции с установленным флагом CIDL_NETWORK, но открывающееся окно для выбора узла и необходимость "раскрывать плюсики" в локальных группах меня не устраивает. Если можно, в API без MFC. - DevXarT

|A1 Необходимо подключить заголовочные файлы
#include <lmcons.h>
#include <lmserver.h>
#include <lmerr.h>
и библиотеку NetAPI
в диалоге "Project Settings" на странице "Link" в поле "Object/library modules:" вписать netapi32.lib

Далее, например так:

void CTestDlg::RefreshHostList()
{
   LPSERVER_INFO_100  pServerEnum;
   DWORD   dwResult, dwRead, dwTotal;

   dwResult = ::NetServerEnum( NULL, 100,
                         (BYTE**) &pServerEnum,
                          -1, &dwRead, &dwTotal,
                         SV_TYPE_ALL, NULL, 0 );

   if ( dwResult == NERR_Success ){
      for ( DWORD i=0; i<dwRead; i++ )
         m_wndListBox.AddString( CString( (LPCWSTR) pServerEnum[ i ].sv100_name ) );
   }
}
Причем, используя SERVER_INFO_101 можно получить более подробную информацию (например тип и версию операционной системы), а комбинируя различные флаги в седьмом параметре NetServerEnum можно выбирать компьютеры по определенному признаку (например, только SQL-серверы или Terminal Server).
Недостаток такого способа в том, что он получает список хостов от мастер-браузера. Таким образом в этом списке присутствуют только хосты, в настоящий момент присутствующие в сети. А поскольку мастер-браузер обновляет эту информацию с периодичностью около 15 минут, список может быть не актуален на данный момент. Кроме того в нем отсутствуют "скрытые" хосты (например командой net config server /hidden:yes ).

А вот мой вопрос... Многие утилиты Windows NT Server (regedt32, Windows NT Diagnostics, Event Viewer, Perfomance Monitor, Shutdown Manager) имеют диалог "Select Computer". Наверняка он в системе "стандартный". Что-то типа SHBrowseForFolder. Может кто знает, где его найти, как вызвать?

- Андрей

|A2 Ответ кроется в группе функций с префиксом ::WNetХХХ:
WNetOpenEnum(RESOURCE_CONTEXT, RESOURCETYPE_ANY, 0, NULL, &handleEnum) - открыть нумерацию локальных доменов верхнего уровня (включая узел Entire Network, эквиалентно выбору Network Neighbourhoods в Explorer), четвертый параметр имеет тип LPNETRESOURCE, где NETRESOURCE - структура, описывающая узел;
WNetOpenEnum(RESOURCEUSAGE_CONTAINER, RESOURCETYPE_ANY, 0, pNetCurrent, &handleEnum) - открыть нумерацию ресурсов узла (шаринги, локальные домены следующего уровня, принтеры, см. флажки в МСДН);
WNetEnumResource(handleEnum, &dwCounter, pNetResource, &dwBufferSize) - получить список ресурсов узла, handleEnum получается предыдущей ф-цией.

... я бы не советовал заполнять листбокс всеми именами машин за раз, процесс этот может быть довольно длительным во времени (порядка минуты); если сеть достаточно велика (от 30-50 машин), лучше использовать дерево.

- James Nicolas Borodco

|A3 Список машин, их имена, имена провайдера, тип подключения и т.д. имеется в реестре. Смотри ключи:
HKEY_CURRENT_USER\Network
HKEY_CURRENT_USER\Network\Recent
Функции для работы с реестром имеются, ищи в MSDN Library, например, RegOpenKeyEx, RegQueryInfoKey. Там же в MSDN Library имеются и примеры работы с реестром (в обзорах, конечно).

- Виктор Никитенко
К сведению: Не во всех системах есть такие ключи реестра. В Windows NT/2000, например, их нет.

Хочу поблагодарить всех, кто откликнулся и прислал ответ на вопрос. Если хотите чтобы ваш ответ был опубликован - постарайтесь, чтобы он был лучше других! Напоминаю, что только вопросы от авторов опубликованных ответов и материалов имеют в дальнейшем приоритет. (Вопросы, естественно, должны быть по программированию).

/ / / В ПОИСКАХ ИСТИНЫ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Q| Как создать такое окно (или это диалогбар?) как Workspace в Visual Studio? То есть, я представляю, что если это диалогбар со списком, то какие стили применить в Create, чтобы его можно было "переносить" и изменять размер? - СашА

Ответить на вопрос

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Начинающим программистам рекомендую подписаться на дружественную рассылку:

Практикум программирования на C++ под Windows
Учебный курс по программированию на языке C++ под Windows. Предназначена для тех, кто уже (немного) умеет программировать, но не знает языка C++ или идеологии написания программ под Windows. В планах - изучение Win32 API, MFC, терминологии Windows и технологий, связанных с ней. Ответы на вопросы, глоссарий, приложения.

Будьте здоровы!

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


No.20

Выпуск No. 20 от 22 октября 2000 г.

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

Тем, кто еще пребывает в блаженном неведении, спешу сообщить хорошую новость.
Рассылка в октябре приобрела статус "золотой"!
Что это значит? По определению Subscribe.Ru, это означает, в частности, что рассылка:

- содержит свежие, актуальные и исключительно полноценные материалы;
- соблюдает указанную в описании тематику и периодичность;
- выходит на грамотном русском языке.

Скромно хочу отметить, что "Программирование на VC++" сейчас единственная "золотая" рассылка в разделе "программирование". Будем надеяться, что этот статус теперь закрепится за ней надолго.
Поздравления принимаются по все тому же адресу ;)

/ / / / НОВОСТИ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Судя по всему, новая концепция Microsoft - .NET - довольно сильно интересует всех программистов, и, соответственно, читателей рассылки. Сегодня я вам предлагаю статью, присланную Ярославом Говоруновым. Он ее решил оформить как продолжение моей публикации из выпуска No. 8.

Что дядя Билли нам готовит
 Часть вторая

Итак, NGWS SDK pre-beta вышла. Это немного развеяло туман, связанный с появлением на свет следующей версии Visual Studio и теперь можно более конкретно говорить о том, что нас ждет. Что же вызвало столько шума? Это не новый язык C#, и не новые инструментальные средства, по большому счету это даже не новая VS ;). Имя счастливчика - .NET.

Так что же такое .NET - технология, SDK или модель? На эту тему было много споров. Был даже спор, является ли .NET операционной системой. Лично я согласен с самим производителем и считаю что .NET - это платформа. Можно сказать, что .NET представляет собой этикетку, название, придуманное маркетологами, для целого набора технологий, как Windows DNA. Формально ее можно определить так:

.NET = COM+
+ дополнительные сервисы и технологии
+ Common Language Runtime ( CLR )
+ набор спецификаций (в т.ч. Common Language Specification -CLS )
+ огромная библиотека объектов.

Концептуально .NET представляет собой единение основных идей, лежащих в основе Java и COM.

Теперь обо всем по порядку.

Ядром всей системы является Common Language Runtime (CLR) - это аналог JVM (Java Virtual Machine), но методы ее работы больше похожи на COM. Она контролирует всю основную работу по выделению и освобождению памяти, созданию и уничтожению объектов, вызову методов и многое другое. При этом на низком уровне используются хорошо известные концепции, такие как контексты объектов, перехват по необходимости, Proxy/Stub и т.д. Большая часть технологий не так нова, как кажется. Например, так рекламируемый 'garbage collection' представляет собой просто красивую оболочку механизма подсчета ссылок в COM, только теперь CLR берет на себя всю рутинную работу.

Однако есть ряд существенных отличий. Как и в случае с Java, .NET программы не компилируются в машинные коды. Вместо этого программа поставляется в виде Intermediate Language (IL). На выходе получается тот же самый exe или dll файл, но вместо машинных кодов он содержит IL. На вид IL очень похож на некий прообраз ассемблера, так что исходные тексты , возможно, останутся защищенными. В отличие от байт-кода, IL-код не может быть интерпретирован. Для выполнения программы используется Just-In-Time Compilation (JITting), когда куски кода компилируются и оптимизируются во время выполнения. Такой метод предположительно будет использоваться для WEB-приложений, так как приводит к потерям производительности. Для пользовательских программ будет использоваться другой - pre-JITing, когда компиляция происходит во время установки программы на пользовательскую машину. IL не зависит от языка программирования, теоретически его можно писать даже вручную, однако .NET предлагает лучшее решение.

Common Language Specification (CLS). Да, хорошо сформулированные спецификации являются, пожалуй, самой сильной стороной .NET. Как известно, ни одна технология или платформа не может стать стандартом без спецификаций. .NET, как и COM, является языконезависимой платформой. CLS определяет набор спецификаций, которым должен соответствовать язык программирования, чтобы стать частью .NET. Разумеется, язык должен быть объектно-ориентированным, он должен поддерживать пространства имен. Запрещается множественное наследование, вместо этого вводится концепция интерфейса и множественное наследование интерфейса. Все это делает разницу между языками достаточно тривиальной. В наши дни, для написания программ, программисты пользуются библиотеками объектов. .NET имеет одну большую библиотеку объектов для всех языков. Точнее библиотека является частью CLR, и соответственно доступна для всех языков в платформе. Библиотека эта очень велика. Она состоит из множества пространств имен, каждое из которых в свою очередь содержит классы или другие пространства имен.[Здесь мне почему-то приходит в голову высказывание Роберта Хайнлайна о том, что слон - это мышь, выполненная по государственным спецификациям ;) - AJ]

И наконец о месте VC в новой платформе. Хочу обрадовать читателей, что для VC была отведена особая роль. Так как сам язык C++ не очень соответствует спецификации CLS, то его пришлось немного изменить. Такой измененный язык называется VC managed extension. Однако это не главное! Главное то, что VC остается единственным средством для производства 'unmanaged code', т.е. <по старинке> - программа компилируется в машинные коды и работает не под управлением CLR, а сама по себе.

Итак, какие же преимущества мы получим с переходом на .NET? Первое, и главное - платформонезависимость. Хоть на данный момент доступна только одна платформа - Windows 2000, Microsoft обещает, что CLR будет доступна для всех основных платформ. Второе - языконезависимость. .NET программы могут разрабатываться на любых из более чем 40 уже доступных языков. При этом предоставляется очень высокий уровень интеграции. Облегчается задача разработчика, так как CLR берет на себя часть рутинных задач. А также большая и удобная в использовании библиотека объектов.

А недостатки? О недостатках пока говорить рано. Как говориться <знал бы, где упал :>.

Возможно, мне так и не удалось ответить на вопрос - что же такое .NET? Грандиозный успех, или грандиозный провал - время покажет. Ясно одно, .NET - это самый значительный шаг Microsoft за последнее время. Важнее даже чем Windows 2000 и X-Box. Грядет революция, и рано или поздно нам придется с этим считаться. Мое мнение - лучше рано, чем поздно.

Несколько полезных ссылок:
Тут можно скачать NGWS SDK
Сайт с кучей полезных ресурсов по .NET
Также сайт посвященный C#
И разумеется первоисточник

/ / / / ОБРАТНАЯ СВЯЗЬ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Здраствуйте Алекс. Спасибо за Вашу рассылку, я с удовольствием читаю ее с первого выпуска. Прочитав выпуск No. 19 решил обратить Ваше внимание на, как мне кажется, более новый способ создания паналей инструментов различного внешнего вида (а'ля Internet Explorer и т.п.) Для этих целей в MFC появился класс CReBar позволяющий размещать на панели инструментов не только кнопки но и практически любые объекты произошедшие от CWnd и имеющие стиль WS_CHILD. Как правило в качестве таких объектов выступают экземпляры классов CToolBar и CDialogBar. Более подробно об этом можно прочитать в MSDN.

- D. Kosyrevsky.

Использование CReBar действительно в некоторых случаях оправданно. Но необходимо знать, что у ReBar существует ряд существенных ограничений. В частности, они не могут быть "плавающими" и стыковаться с границами окна.

Что касается постановки вопроса: "сделать тулбар а'ля WinZip - большие иконки, с подписями...", так это можно сделать в редакторе ресурсов просто "растянув" изображение до нужного размера и написав все, что нужно (естественно, надписи будут статическими). Что же касается размещения элементов управления на панели инструментов за счет "расширения" сепараторов, то это действительно интересно и, главное, более гибко и удобно, чем создание DialogBar.

- Евгений Шмелев.

Вряд ли статический (т.е. являющийся частью изображения ) текст кого-либо устроит. Это, конечно, можно сделать, но вообще-то это не принято.

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

Q| Как создать такое окно (или это диалогбар?) как Workspace в Visual Studio? То есть, я представляю, что если это диалогбар со списком, то какие стили применить в Create, чтобы его можно было "переносить" и изменять размер? - СашА

На этот вопрос было получено удивительно мало ответов. Неужели этим почти никто не интересовался?

|A1 Среди заголовочных файлов MFC есть заголовочный файл afxpriv.h, в котором объявлено несколько недокументированных классов, в том числе например класс CDockBar. По-моему именно он обеспечивает создание окон в стиле Visual Studio.

- Anton

Я хотел бы предостеречь читателей: использование недокументированных классов чревато неприятностями. В том же MSDN написано, что члены и методы класса CDockBar, скорее всего, претерпят сильные изменения в будущем.

|A2 Во время оно я боролся с этим вопросом. Вот краткие выводы: DialogBar - ДЕРЬМО. Проще сходить на сайт http://www.datamekanix.com и слить оттуда компонент CSizingControlBar написанный Crisite Posea. Он тоже не предел совершенства, но работает почти так же как и Workspace.

- Vassili Bourdo

Хочу добавить (как еще один вариант), что я кажется видел класс с такой же функциональностью в Ultimate Toolbox от Dundas Software.

/ / / В ПОИСКАХ ИСТИНЫ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Q| У меня есть приложение MFC на базе диалога. Я решил организовать переключение некоторых режимов через главное меню, т.е. в меню присутствуют названия режимов и активный в данный момент режим помечен точкой. Для этого я создал обработчики ON_UPDATE_COMMAND_UI для соответствующих пунктов меню и в них вызов СCmdUI::SetRadio(). Например:

void CHeatDlg::OnUpdateSolve(CCmdUI* pCmdUI)
{
     if(mode == 1) pCmdUI->SetRadio(TRUE);
     else pCmdUI->SetRadio(FALSE);
}
Это не сработало. Похоже, что сообщения ON_UPDATE_COMMAND_UI просто не посылаются. До этого я использовал такой же подход в приложениях SDI и MDI и все работало. Есть какие-нибудь мысли по этому поводу? - Андрей Моисеев

Ответить на вопрос

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Успехов!

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


No.21

Выпуск No. 21 от 29 октября 2000 г.

Все настоящие программисты делятся на три
категории: на тех, кто пишет программы,
завершающиеся по нажатию F10, Alt-F4 и Alt-X.
Все остальные принципы деления надуманны
- авт. неизв.

Рад снова приветствовать вас!

Сегодняшний выпуск посвящен вашим письмам с вопросами, ответами, комментариями, замечаниями и дополнениями, которых у меня накопилось изрядное количество.

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

Q| У меня есть приложение MFC на базе диалога. Я решил организовать переключение некоторых режимов через главное меню, т.е. в меню присутствуют названия режимов и активный в данный момент режим помечен точкой. Для этого я создал обработчики ON_UPDATE_COMMAND_UI для соответствующих пунктов меню и в них вызов СCmdUI::SetRadio(). Например:

void CHeatDlg::OnUpdateSolve(CCmdUI* pCmdUI)
{
     if(mode == 1)
       pCmdUI->SetRadio(TRUE);
     else
      pCmdUI->SetRadio(FALSE);
}
Это не сработало. Похоже, что сообщения ON_UPDATE_COMMAND_UI просто не посылаются. До этого я использовал такой же подход в приложениях SDI и MDI и все работало. Есть какие-нибудь мысли по этому поводу? - Андрей Моисеев
|A1 Решение проблемы обработчика ON_UPDATE_COMMAND_UI частично кроется в одном из предыдущих выпусков - No.18. Ведь этот обработчик вызывается Framework'ом MFC из OnIdle объекта приложения, а в dialog-based программах OnIdle не работает. Тут можно посоветовать использовать в качестве главного окна аппликации скрытое "фиктивное" окно и подсунуть его в качестве родителя диалога.

- Sergey Emantayev

|A2 Дело в том, что вся логика генерации user-interface update command message для меню (создание объекта СCmdUI и т.д.) реализована в CFrameWnd::OnInitMenuPopup. Соответственно в dialog-based приложениях вызов этой функции отсутствует. Очевидно, предполагалось что в диалогах меню не обязательно. Поэтому можно предложить два варианта. Общим в них является необходимость создания обработчика OnInitMenuPopup для вашего диалога. Первый вариант подойдёт, если вы уже повсеместно расставили обработчики ON_UPDATE_COMMAND_UI, и не хотите ничего переделывать: Вы просто копируте тело функции CFrameWnd::OnInitMenuPopup в созданную вами функцию (благо, что исходники доступны), чистите всё лишнее (разобраться достаточно легко) и созданные вами обработчики OnUpdateХХХ начинают вызываться Второй вариант предполагает, что вы, что называется "ручками", изменяете состояния пунктов меню прямо в созданной вами выше функции OnInitMenuPopup, используя передаваемый ей как параметр CMenu*. Этот вариант вполне приемлем, если меню не очень большое и структура его не очень разветвлённая. Для некоторых этот вариант также покажется более привлекательным по той причине, что вы всё делаете сами, а не копируете фрагменты чужого кода.

- Роман Клепов

|A3 Когда Windows собирается отобразить всплывающее меню, она посылает сообщение WM_INITMENUPOPUP, чтобы программа могла на лету кое-что поменять: отключить некоторые пункты, расставить галочки и т. п. Поэтому базовый способ модификации всплывающих меню состоит в перехвате этого сообщения с последующим использованием функций EnableMenuItem, CheckMenuItem и т. п.

MFC предлагает альтернативный подход. С классе CFrameWnd есть готовый обработчик WM_INITMENUPOPUP, который инициализирует структуру CCmdUI для каждого пункта меню, после чего отправляет сообщение CN_UPDATE_COMMAND_UI, определённое в MFC, сперва классу представления, затем классу документа, затем классу главного окна и, наконец, классу приложения. Каждый из этих классов может внести свою лепту в инициализацию всплывающего меню. Можно инициировать этот процесс, вообще ничего не зная о CN_UPDATE_COMMAND_UI: достаточно вызвать CCmdUI::DoUpdate, и сообщеине дойдёт до написанных программистом обработчиков ON_UPDATE_COMMAND_UI.

Теперь внимание: класс диалога (CDialog) не имеет предопределённого обработчика WM_INITMENUPOPUP. Поэтому сообщения CN_UPDATE_COMMAND_UI никто не посылает, и обработчики ON_UPDATE_COMMAND_UI не вызываются. Необходимо вручную написать обработчик OnInitMenuPopup, воспроизведя в нём часть функциональности класса CFrameWnd. В простейшем случае он может выглядеть так:

void CMyDlg::OnInitMenuPopup(CMenu* pPopupMenu, UINT nIndex, BOOL bSysMenu)
{
   if (!bSysMenu)
   {
    CCmdUI state;

    state.m_pMenu = pPopupMenu;
    state.m_nIndexMax = pPopupMenu->GetMenuItemCount();

    for (state.m_nIndex = 0; state.m_nIndex < state.m_nIndexMax; state.m_nIndex++)
     {
       state.m_nID = pPopupMenu->GetMenuItemID(state.m_nIndex);
       state.m_pSubMenu = NULL;
       state.DoUpdate(this, state.m_nID < 0xF000);
      }
  }
}

В случае когда всплывающее меню имеет подменю обработчик будет более сложным. За примером такого обработчика можно обратиться к исходным текстам программы DLGCBR32, которые находятся в MSDN.

- Alexander Shargin

|A4 Это и не должно работать, так как диалоговое окно не посылает сообщение на обновление меню, следует заметить, что такие "казусы" происходят с диалогами достаточно часто, к примеру Диалог не посылает сообщение о командах меню своим детям, как это делают приложениях SDI и MDI, все дело в том что в MFC диалоги работают по другим правилам, чем обычные окна, так как в MFC главный упор сделан на архитектуру Document-View, а диалоги "искуственное" добавление. Я решал подобную проблему, самостоятельно посылать сообщение Меню на обновление, так:

BEGIN_MESSAGE_MAP(CMyDlg, CDialog)
...
  ON_MESSAGE(WM_KICKIDLE, OnKickIdle)
...
END_MESSAGE_MAP()

LRESULT CMyDlg::OnKickIdle(WPARAM w, LPARAM l)
{
   Sleep(50);
   // Самостоятельно обновлять меню ...
   UpdateMenu( this , GetMenu());
   return TRUE;
}

// А это моя функция ...

void UpdateMenu(CWnd *pWnd, CMenu *pMenu)
{
  CCmdUI cmdUI;

  cmdUI.m_pMenu = pMenu;
  cmdUI.m_nIndexMax = pMenu->GetMenuItemCount();

  for (cmdUI.m_nIndex = 0; cmdUI.m_nIndex < cmdUI.m_nIndexMax; ++cmdUI.m_nIndex)
  {
     CMenu* pSubMenu = pMenu->GetSubMenu(cmdUI.m_nIndex);
     if (pSubMenu == NULL)
     {
      cmdUI.m_nID = pMenu->GetMenuItemID(cmdUI.m_nIndex);
      cmdUI.DoUpdate(pWnd, FALSE);
     }
     else
       UpdateMenu(pWnd, pSubMenu);
   }
}/// и все.

- Oleg Zhuk

Ответов на этот вопрос пришло довольно много, и я постарался отобрать самые лучшие из них. Заметьте, что хотя некоторые ответы похожи, в реализации имеются серьезные различия.

Авторам всех ответов, и опубликованных, и не опубликованных, большое спасибо!

/ / / / ОБРАТНАЯ СВЯЗЬ / / / / / / / / / / / / / / / / / / / / /

Из входящей почты:

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

- Юрий Карпенко

Ну, в чем-то возможно и справедливо. Но далеко не во всем. Некоторые не могут свободно работать с MSDN или CodeGuru из-за недостаточного знания английского, или жалко времени в интернет, а кому-то просто лень..
И потом, я думаю, ответы на вопросы полезны не только для тех, кто задал вопрос - самое главное, они полезны тем, кто даже с этой проблемой никогда и не сталкивался. Рассылка позволяет им получать новые познания практически без труда. Ведь доказано, что в школе ученик, понявший новую тему раньше своего товарища, сможет ему ее объяснить гораздо лучше, чем сам учитель. В случае, если у кого-то в будущем возникнет такая задача, он уже будет знать, в каком направлении двигаться при ее решении.

Следующее письмо пришло в ответ на те строки, которые я написал в прошлом выпуске о классе CReBar. Напомню: "Использование CReBar действительно в некоторых случаях оправданно. Но необходимо знать, что у ReBar существует ряд существенных ограничений. В частности, они не могут быть "плавающими" и стыковаться с границами окна."

Полегче со словом "не могут", уважаемый.
http://codeproject.com/docking/tearoffrebars.asp
Вот яркий пример того чего не может быть.

- Paul Bludov

Тот контрол, на который сослался автор письма, в самом деле, благодаря усилиям программиста, который его создал, может "плавать". Но полной функциональности тулбаров он все равно не достигает - стыковаться он по-прежнему может только с одной границей, не возвращается на свое место по double-click и т.д. И потом, я говорил о CReBar. Вряд ли этот модифицированный класс можно так назвать.
Так что полегче со словом "полегче", уважаемый Павел...

Еще письма:

Есть комментарий к ответам на вопрос из номера 18 (по поводу списка компьютеров в сети), точнее к первому из них.
-------------------------
В документации Микрософт сказано, что Функции Netxxx устарели и следует пользоваться функциями WNetxxx. Во-вторых, с использованием функций Netxxx есть проблемы из-за того, что для Windows NT и Windows 9x используются различные библиотеки (в первом случае netapi32.lib, во втором svrapi.lib). Также вызовы функций в этих библиотеках различаются параметрами (кстати, в MSDN приводится версия для WinNT, а для Win9x придется читать заголовочный файл svrapi.h). Кстати, для Windows 9x следует использовать именно svrapi.h, а не lmxxx.h.

- Sergey Shoumko

Тут вспомнилось - к Вашему выпуску о читабельности кода:
"Отсутствие коментариев в программе - веский повод для увольнения программиста" - Дональд Кнут

- Роман

/ / / В ПОИСКАХ ИСТИНЫ / / / / / / / / / / / / / / / / / / / / / /

Q| Все, наверное, знают программы, называемые Viewbar, которые показывают рекламные баннеры. Но вот как они ограничивают часть экрана, не позволяя другим окнам находиться поверх них? Например, если разрешение экрана 800x600, как они выделяют полосу сверху, в которой находятся, т.ч. программы, развернутые на полный экран, имеют высоту где-то на 60 пикселей меньше. Причем и немаксимизированные окна не могут "влезть" в эту полосу. - Alexander Popov

Ответить на вопрос

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

До встречи!

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


No.22

Выпуск No. 22 от 5 ноября 2000 г.

Здравствуйте!

Мне пришла пара писем от подписчиков, где они выразили некоторую неудовлетворенность существующей в рассылке системой поощрения авторов лучших ответов и статей (для вновьподписавшихся: см. в архиве выпуск No. 18) Они пишут, что "прежде всего надо публиковать самые интересные вопросы."

Я лично с этим целиком и полностью согласен, и всегда фактически стараюсь так и делать, хотя понятие "интересный вопрос" достаточно размыто и каждый понимает его по-своему. Для некоторых, например, интересный вопрос - "Как связать контролы на диалоге с переменными класса?", а для других - ...мм, ну, совершенно другое ;)

С другой стороны, у меня нет абсолютно никакой альтернативы для поощрения авторов, кроме как морального поощрения. Думаю мало кого увлечет обещание типа "Вы увидите свое имя в рассылке, оно навечно войдет в скрижали ее истории", и т.д. и т.п.

Так вот, к чему я клоню. РАССЫЛКЕ НУЖЕН ПОСТОЯННЫЙ СПОНСОР И РЕКЛАМОДАТЕЛЬ. Тогда станет возможно назначить материальное вознаграждение за лучший ответ и лучший материал (а, возможно, и лучший вопрос тоже!)

Это будет выгодно прежде всего самим читателям, поскольку качество и интересность вопросов, ответов и статей значительно повысится. Да и авторам, я думаю тоже будет неплохо получить, скажем, энное количество $ потратив несколько минут на ответ.

И тогда, действительно, будет возможно публиковать только САМЫЕ ИНТЕРЕСНЫЕ вопросы.

Так что дело только за вами, уважаемые рекламодатели! Хочу вам напомнить, что рассылку получают около 8500 интересующихся программированием человек.

К читателям: может, у вас есть какие-нибудь идеи или просто интересные мысли по этому поводу? Не стесняйтесь - пишите мне.

/ / / / CТАТЬЯ / / / / / / / / / / / / / / / / / /

Сегодня я предлагаю вам заметку, написанную уже воистину постоянным автором нашей рассылки - Александром Шаргиным.

Три способа подключения DLL

Многие знают, что существует два основных способа подключить DLL к выполняющемуся процессу - явный и неявный.

При неявном подключении (implicit linking) линкеру передаётся имя библиотеки импорта (с расширением lib), содержащей список функций DLL, которые могут вызвать приложения. Обнаружив, что программа обращается к одной из этих функций, линкер добавляет информацию о содержащей её DLL в целевой exe-файл. Позже, когда этот exe-файл будет запущен, загрузчик попытается спроектировать необходимую DLL на адресное пространство процесса; в случае неудачи весь процесс будет немедленно завершён.

При явном подключении (explicit linking) приложение вызывает функцию LoadLibrary(Ex), чтобы загрузить DLL, затем использует функцию GetProcAddress, чтобы получить указатели на требуемые функции, а по окончании работы с этими функциями вызывает FreeLibrary, чтобы выгрузить библиотеку и освободить занимаемые ею ресурсы.

Каждый из способов имеет свои достоинства и недостатки. В случае неявного подключения все библиотеки, используемые приложением, загружаются в момент его запуска и остаются в памяти до его завершения (даже если другие запущенные приложения их не используют). Это может привести к нерациональному расходу памяти, а также заметно увеличить время загрузки приложения, если оно использует очень много различных библиотек. Кроме того, если хотя бы одна из неявно подключаемых библиотек отсутствует, работа приложения будет немедленно завершена. Явный метод лишён этих недостатков, но делает программирование более неудобным, поскольку требуется следить за своевременными вызовами LoadLibrary(Ex) и соответствующими им вызовами FreeLibrary, а также получать адрес каждой функции через вызов GetProcAddress.

В Visual C++ 6.0 появился ещё один способ подключения DLL, сочетающий в себе почти все достоинства двух рассмотренных ранее методов - отложенная загрузка DLL (delay-load DLL). Отложенная загрузка не требует поддержки со стороны операционной системы (а значит будет работать даже под Windows 95), а реализуется линкером Visual C++ 6.0.

При отложенной загрузке DLL загружается только тогда, когда приложение обращается к одной из содержащихся в ней функций. Это происходит незаметно для программиста (то есть вызывать LoadLibrary/GetProcAddress не требуется). После того как работа с функциями библиотеки завершена, её можно оставить в памяти или выгрузить посредством функции __FUnloadDelayLoadedDLL. Вызов этой функции - единственная модификация кода, которую может потребоваться сделать программисту (по сравнению с неявным подключением DLL). Если требуемая DLL не обнаружена, приложение аварийно завершается, но и здесь ситуацию можно исправить, перехватив исключение с помощью конструкции __try/__except. Как видим, отложенная загрузка DLL - весьма удобное средство для программиста.

Теперь рассмотрим, каким образом описанные способы подключения DLL используются на практике. Для этого будем считать, что нам требуется вызвать функцию X, экспортируемую библиотекой MyLib.dll. Пусть функция X имеет простейший прототип: void X(void);

Будем также считать, что библиотека импорта находится в файле MyLib.lib.

Неявное подключение
Первое, что нам нужно сделать - это передать линкеру имя библиотеки импорта нашей DLL. Для этого необходимо открыть окно настройки проекта(Project->Settings) и на вкладке Link дописать "MyLib.lib" в конец списка Object/Library modules. Альтернативный подход заключается в использовании директивы #pragma. В нашем случае необходимо вставить в код программы следующую строку:

#pragma comment(lib,"MyLib")

Второе, что нужно проделать - это добавить объявление функции в код программы (обычно объявления функций, экспортируемых DLL, сводятся в заголовочный файл - тогда требуется просто подключить его). Для нашей функции X объявление выглядит так:

__declspec(dllimport) void X(void);

Вот и всё. Теперь к функции X можно обращаться, как и к любой другой функции, статически прилинкованной к нашей программе:

X();

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

Загружаем библиотеку:

HINSTANCE hLib = LoadLibrary("MyLib.dll");

Получаем указатель на функцию и вызываем её:

void (*X)();

(FARPROC &)X = GetProcAddress(hLib, "X");

X();

Выгружаем библиотеку из памяти:

FreeLibrary(hLib);

Отложенная загрузка
Сначала необходимо повторить шаги, которые мы проделывали при неявном подключении: передать линкеру имя библиотеки импорта и добавить в программу объявление функции X. Теперь, чтобы отложенная загрузка заработала, нужно добавить ключ линкера /DELAYLOAD:MyLib.dll и прилинковать к приложению библиотеку Delayimp.lib, реализующую вспомогательные функции механизма отложенной загрузки. Хотя эти опции можно добавить в настройки проекта, я предпочитаю использовать директивы #pragma:

#pragma comment(lib, "Delayimp")
#pragma comment(linker, "/DelayLoad:MyLib.dll")

Если после вызова функции X нам требуется выгрузить библиотеку MyLib.dll из памяти, можно воспользоваться функцией __FUnloadDelayLoadedDLL. Чтобы эта функция работала корректно, необходимо добавить ещё один ключ линкера /DELAY:unload. Кроме того, нужно подключить заголовочный файл , в котором эта функция объявлена. Выглядеть это может примерно так:

#include <Delayimp.h>
#pragma comment(linker, "/Delay:unload")
.
.
X();

__FUnloadDelayLoadedDLL("MyLib.dll");

Имя, передаваемое функции __FUnloadDelayLoadedDLL, должно в точности совпадать с именем, указанным в ключе /DELAYLOAD. Так, если передать ей имя "MYLIB.DLL", библиотека останется в памяти.

В заключение хочется отметить ещё один интересный момент. Когда я попытался воспользоваться отложенной загрузкой в своей программе, линкер отказался подключать библиотеку Delayimp.lib, выдавая сообщение о внутренней ошибке и подробную отладочную информацию. Чтобы решить эту проблему, я просто взял файлы Delayhlp.cpp и Delayimp.h из каталога Vc98\Include, добавил в файл Delayhlp.cpp строки:

PfnDliHook __pfnDliNotifyHook = NULL;
PfnDliHook __pfnDliFailureHook = NULL;

и перестроил эту библиотеку заново. После этого отложенная загрузка заработала нормально.

Ссылки
Поскольку я рассказал об отложенной загрузке далеко не все, рекомендую обратиться за дополнительной информацией к следующим статьям в MSDN:

- December 1998, Microsoft systems journal, Win32 Q&A
- December 1998, Microsoft systems journal, Under the hood
- Linker support for delay-loaded DLLs

- Alexander Shargin (rudankort@mail.ru)

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

Q| Все, наверное, знают программы, называемые Viewbar, которые показывают рекламные баннеры. Но вот как они ограничивают часть экрана, не позволяя другим окнам находиться поверх них? Например, если разрешение экрана 800x600, как они выделяют полосу сверху, в которой находятся, т.ч. программы, развернутые на полный экран, имеют высоту где-то на 60 пикселей меньше. Причем и немаксимизированные окна не могут "влезть" в эту полосу. - Alexander Popov

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

|A1 Спасибо за опубликование вопроса. Теперь я сам же могу на него ответить. Для создания приложения, похожего на панель задач или панель MS Office, используется AppBar. Последний достаточно хорошо описан в MSDN (см. Extend the Windows 95 Shell with Application Desktop Toolbars, Application Desktop Toolbars) А вообще, достаточно много интересного содержится в Windows Shell API, в частности: работа с панелью задач, как написать ScreenSaver, работа с панелью управления, Band Objects в Internet Explorer.

- Alexander Popov

|A2 Этого можно добиться, используя функцию SystemParametersInfo. У этой исключительно полезной функции существуют параметры SPI_GETWORKAREA и SPI_SETWORKAREA, позволяющие получить размер рабочей области экрана или установить для неё собственный размер (перед завершением работы программы его рекомендуется восстановить). Напрмер, следующий фрагмент "резервирует" полосу шириной в 100 пикселей в верхней части экрана:

CRect rcOld, rcNew;
SystemParametersInfo(SPI_GETWORKAREA, 0, (PVOID)&rcOld, 0);
rcNew = rcOld;
rcNew.top = 100;
SystemParametersInfo(SPI_SETWORKAREA, 0, (PVOID)&rcNew, 0);

Чтобы восстановить исходный размер, достаточно вызвать:

SystemParametersInfo(SPI_SETWORKAREA, 0, (PVOID)&rcOld, 0);

После того как нужная область зарезервирована, можно, например, поместить туда своё окно (вызовом CWnd::MoveWindow) и лишить пользователя возможности убрать его оттуда (так как в противном случае оно туда не вернётся), после чего рисовать в нём баннеры.
В заключение отмечу, что именно этой функцией пользуются программы типа Magnify.exe из комплекта Windows.

- Alexander Shargin (rudankort@mail.ru)

Фактически, ответы, конечно, одинаковые (в статье из MSDN как раз и используется SystemsParametersInfo), просто первый в отличие от самого ответа содержит ссылку на него.

Эти два ответа - все, что я получил. Два из восьми тысяч. Действительно, не очень-то сильно читатели хотят отвечать на вопросы. Так что я по всей видимости был прав по поводу поощрений... Господа! Прошу поактивнее! Или я могу решить что рубрика вам неинтересна и закрою ее...

Многие спрашивают, почему я лично не отвечаю на вопросы. Это неправда, иногда все-таки отвечаю ;)
Ну а главное: посмотрите Microsoft Systems Journal. Там человек В МЕСЯЦ отвечает на ДВА вопроса, причем это - его работа, т.е. он получает за это деньги. Потом, далеко не на всякий вопрос можно сходу дать однозначный ответ. Как правило, те вопросы, для которых можно это сделать - неинтересны. Так что наверное гораздо эффективнее разделять эту задачу с читателями.

/ / / / ОБРАТНАЯ СВЯЗЬ / / / / / / / / /

Alexander Shargin по поводу ответа A1 (Sergey Emantayev) из No.21 пишет:

Справедливости ради следует отметить, что всплывающие меню не обновляются в Idle loop'е. В нём обновляются тулбары, статус бар и т. п., но всплывающие меню обновляются только в ответ на WM_INITMENUPOPUP. Этой практики следует придерживаться и в собственных приложениях.
Продолжается дискуссия о комментариях:
Привет, что касается комментариев, то не кажется ли вам, господа, что сам код (естественно, грамотный код) и является самым лучшим и лаконичным комментарием к программе. И вообще, мне кажется, что комментарии нужны, только тем людям, которые не писали конкретную программу, и причем, комментировать следует, только, ключевые моменты, а если человек просто не понимает кода, то комментируй, не комментируй, это по барабану. Года два назад, я писал, одну небольшую софту, она работала под DDraw в полно-экранном режиме, я колбасил интерфейс a-la X-window Linux, фунций, переменных было море, и я закоментировал только, что и какая переменная делает, недавно, пришлось вернутся к этому коду, я его передал другому, человеку, немного видоизменив. И никаких проблем с восстановлением (в мозгу) архитектуры программы не было, как будто я сней не работал не два года, а два дня. Код это и сеть лучший комментарий, ну иногда желательно и присутствие "бумажной" модели программы.

- Anton Palagin
У меня к Вам предложение по Вашей рассылки "Vsiual C++" - не могли бы Вы в поле сабжа каждой рассылки в конце ставить темы, которые рассматриваются в данном номере рассылки (например : "WinInet, Tray, Закладки"). Так будет значительно удобнее ориентироваться в архиве рассылке при поиске нужной темы. А то вот искал тот номер, где рассматривался вопрос с помещением программы в Tray и пришлось потратить некоторое время (линейно зависящее от количества вышедших номеров рассылки). А так посмотрел на заголовок и все понятно. А то поиск по сообщениям в Outlook Express глючный какой-то и нифига не находит.

- Даниил Иванов

Очень разумное предложение, на мой взгляд. Принял к исполнению - взгляните на subject этого выпуска. Спасибо, Даниил! Правда, в заголовок выношу только главную тему выпуска. Побочные придется искать по-старому.

/ / / В ПОИСКАХ ИСТИНЫ / / / / / / / / / / /

Q| Все знают десктопные программы-ассистенты (screenmates/deskmates, MS Agent). Весь вопрос, что качественных, без артифактов, достаточно мало. Для реализации экранного помошника есть 2 различных подхода (если знаете еще, подскажите): - рисовать поверх десктопа, запоминать-востанавливать фон и т.д. Здесь сложно уследить за случаями, когда другие окна перекрывают место, где выводится текущий кадр персонажа, если на десктопе идет своя жизнь (меняются-появляются иконки, молчу про Drag'n'Drop) - использовать регионы, примеры есть на codeguru, но это достаточно трудоемкая штука - идея проста: создать полностью прозрачное окно и рисовать в нем просто текущий кадр с действием персонажа, не заботясь о том, на каком фоне его рисуешь, ведь окно прозрачное! Т.е. программа может просто рисовать постоянно меняющиеся картинки в таком прозрачном окне и это создаст эффект анимации персонажа, главное тут, чтобы любые изменения фона не влияли, т.е. просто добавление атрибута WS_EX_TRANSPARENT - это не то что нужно

Так вот, внимание, вопрос!
Знает ли кто, как можно создать такое полностью прозрачное-невидимое окно, которое при перемещении по экрану не тащит за собой кусок фона с предыдущего местоположения?
Кстати, на кодегуру прямого примера нет точно, а то что есть о том как рисовать прозрачные штучки -не то. - Valery Boronin

Ответить на вопрос

/ / / / АНОНС / / / / / / / / / / / / / / / / / / / / /

Читайте в следующем выпуске
Многозадачность в Windows: теория и практика

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Успехов в программировании!

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


No.23

Выпуск No. 23 от 12 ноября 2000 г.

Cын Билла Гейтса приходит к отцу и спрашивает:
"Папа, а что такое многозадачность?"
"Подожди, сынок, вот дискету доформатирую..."

Приветствую вас, уважаемые читатели!

Как я и ожидал, желающих становиться спонсором рассылки пока не нашлось. Ну что ж, ничего не поделаешь - будем придерживаться тогда пока старой системы.

/ / / / СТАТЬЯ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Многозадачность и ее применение
 Или зачем нужна многопоточность простому программисту

Очень многие программисты, перейдя с DOS на Windows, в течение долгого времени все еще стараются программировать по-старому. Конечно, полностью это сделать не получается - такие вещи, как обработка сообщений, являются неотъемлемой частью любого Windows-приложения. Однако, 32-разрядная платформа в силу своей структуры предоставляет программистам новые захватывающие дух возможности. И если вы их не используете, а стараетесь решить проблему так, как привыкли, то вполне естественно, что из этого не получается ничего хорошего.

Эти возможности - возможности многозадачности. Прежде всего очень важно уяснить для себя, КОГДА вам следует подумать об ее использовании в своем приложении. Ответ так же очевиден, как и определение термина "многозадачность" - она нужна тогда, когда вы хотите, чтобы несколько участков кода выполнялось ОДНОВРЕМЕННО. Например, вы хотите, чтобы какие-то действия выполнялись в фоновом режиме, или чтобы в течение ресурсоемких вычислений, производимых вашей программой, она продолжала реагировать на действия пользователя. Я думаю, вы легко сможете придумать еще несколько примеров.

Процессы и потоки
Эти два понятия очень важны, и вы должны постараться их хорошенько осмыслить. Процессом (process) называется экземпляр вашей программы, загруженной в память. Этот экземпляр может создавать потоки (thread), которые представляют собой последовательность инструкций на выполнение. Важно понимать, что выполняются не процессы, а именно потоки. Причем любой процесс имеет хотя бы один поток. Этот поток называется главным (основным) потоком приложения.

Так как практически всегда потоков гораздо больше, чем физических процессоров для их выполнения, то потоки на самом деле выполняются не одновременно, а по очереди. (Заметьте, что распределение процессорного времени происходит именно между потоками, так как, еще раз повторю, процессы не выполняются.) Но переключение между ними происходит так часто, что кажется будто они выполняются параллельно.

В зависимости от ситуации потоки могут находиться в трех состояниях. Давайте посмотрим, что это за состояния. Во-первых, поток может выполняться, когда ему выделено процессорное время, т.е. он может находиться в состоянии активности. Во-вторых, он может быть неактивным и ожидать выделения процессора, т.е. быть в состоянии готовности. И есть еще третье, тоже очень важное состояние - состояние блокировки. Когда поток заблокирован, ему вообще не выделяется время. Обычно блокировка ставится на время ожидания какого-либо события. При возникновении этого события поток автоматически переводится из состояния блокировки в состояние готовности. Например, если один поток выполняет вычисления, а другой должен ждать результатов, чтобы сохранить их на диск. Второй мог бы использовать цикл типа "while( !isCalcFinished ) continue;", но легко убедиться на практике, что во время выполнения этого цикла процессор занят на 100% (это называется активным ожиданием). Таких вот циклов следует по возможности избегать, в чем нам оказывает неоценимую помощь механизм блокировки. Второй поток может заблокировать себя до тех пор, пока первый не установит событие, сигнализирующее о том, что чтение окончено.

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

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

Иногда приоритет потока может изменяться динамически. Так интерактивные потоки, имеющие обычно класс приоритета Normal, система обрабатывает несколько иначе и несколько повышает фактический приоритет таких потоков, когда процесс, их породивший, находится на переднем плане (foreground). Это сделано для того, чтобы приложение, с которым в данный момент работает пользователь, быстрее реагировало на его действия.

Конечно, эта заметка не претендует на исчерпывающее описание использования многозадачности. Цель этой вводной статьи - заинтересовать темой. В дальнейшем мы с вами поговорим о таких вещах, как создание интерфейсных потоков и синхронизация между потоками. А если вы хотите посмотреть пример создания рабочего потока, то он рассматривался в рассылке No.5 от 28 июня.

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

Q| Все знают десктопные программы-ассистенты (screenmates/deskmates, MS Agent). Весь вопрос, что качественных, без артифактов, достаточно мало. Для реализации экранного помошника есть 2 различных подхода (если знаете еще, подскажите): - рисовать поверх десктопа, запоминать-востанавливать фон и т.д. Здесь сложно уследить за случаями, когда другие окна перекрывают место, где выводится текущий кадр персонажа, если на десктопе идет своя жизнь (меняются-появляются иконки, молчу про Drag'n'Drop) - использовать регионы, примеры есть на codeguru, но это достаточно трудоемкая штука - идея проста: создать полностью прозрачное окно и рисовать в нем просто текущий кадр с действием персонажа, не заботясь о том, на каком фоне его рисуешь, ведь окно прозрачное! Т.е. программа может просто рисовать постоянно меняющиеся картинки в таком прозрачном окне и это создаст эффект анимации персонажа, главное тут, чтобы любые изменения фона не влияли, т.е. просто добавление атрибута WS_EX_TRANSPARENT - это не то что нужно

Так вот, внимание, вопрос!
Знает ли кто, как можно создать такое полностью прозрачное-невидимое окно, которое при перемещении по экрану не тащит за собой кусок фона с предыдущего местоположения?
Кстати, на кодегуру прямого примера нет точно, а то что есть о том как рисовать прозрачные штучки -не то. - Valery Boronin

|A1 Нужно для каждой картинки, входящей в анимацию, делать для окна специальный регион, который включал бы в себя точки, принадлежащие изображению и не включал все остальные. Это можно сделать так (source ниже) : Создать пустой регион, выбрать картинку (bitmap), выбрать прозрачный цвет, проитись по bitmap и для каждого непрозрачного участка в каждой строке bitmap создать регион высотой 1 пиксел и прикомбинировать его к исходному региону. В конце операции установить получившийся регион окну.

void MakeBitmapRegion (HWND hwnd, int bmp_id)
{
        COLORREF back_color;
        CBitmap bmp;
        if (!bmp.LoadBitmap (bmp_id)) return;
        BITMAP bmp_o;
        bmp.GetObject (sizeof(BITMAP), (LPSTR)&bmp_o);
        int w = bmp_o.bmWidth;
        int h = bmp_o.bmHeight;

        HDC wnd_dc = GetDC(hwnd);
        if (hwnd   == NULL) return;
        if (wnd_dc == NULL) return;

        HDC hdc_bmp = CreateCompatibleDC (wnd_dc);
        SelectObject (hdc_bmp, HBITMAP (bmp));
        back_color = GetPixel(hdc_bmp, 0, 0);

        int x, x0, y;
        HRGN tmp_rgn, wnd_rgn;
        wnd_rgn = CreateRectRgn(0,0,0,0);

        x = y = 0;
        for (y; y< h; y++)
        {
                while (x < w-1)
                {
                        while(GetPixel(hdc_bmp, x, y) == back_color && x < w) x++;
                        if( x != w )
                        {
                                x0 = x;
                                while(GetPixel(hdc_bmp, x, y) != back_color && x < w) x++;
                                tmp_rgn = CreateRectRgn(x0, y, x, y+1);
                                CombineRgn(wnd_rgn, wnd_rgn, tmp_rgn, RGN_XOR);
                        }
                }
                x = 0;
        }

        DeleteObject(tmp_rgn);
        DeleteDC(hdc_bmp);
        SetWindowRgn(hwnd, wnd_rgn, TRUE);
        DeleteObject(wnd_rgn);
}

- Сергей Егоров

|A2 Как сделать полностью прозрачное окно, которое не тащит за собою кусок фона - понятно. Нужно просто перехватить сообщение WM_WINDOWPOSCHANGING и сказать системе, чтобы она не копировала содержимое окна. Для этого в структуре WINDOWPOS, указатель на которую передаётся в функцию окна, предусмотрен флаг SWP_NOCOPYBITS. В MFC обработчик может выглядеть примерно так:

void CMyWnd::OnWindowPosChanging(WINDOWPOS FAR* lpwndpos)
{
   lpwndpos->flags |= SWP_NOCOPYBITS;
   CWnd::OnWindowPosChanging(lpwndpos);
}

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

Если прозрачные области в окне статические, то есть способ лучше - воспользоваться SetWindowRgn. Об этой функции писалось в 7-м выпуске рассылки. Но если требуется организовать анимацию на фоне рабочего стола, то, вероятно, не обойтись без сохранения фона с его последующим восстановлением. Дело в том, что многие программы очень медленно перерисовывают свои окна, и поручать им обновление фона под нашим окном не представляется возможным.

- Alexander Shargin (rudankort@mail.ru)

/ / / / ОБРАТНАЯ СВЯЗЬ / / / / / / / / / / / / / / / / / / / / / / /

К прошлому выпуску:

У меня есть два меленьких примечания к теме "Три способа подключения DLL":
1. При неявном подключении .lib файл можно добавить к проекту с помощью меню "Project\Add to project\Files", выбрав тип файлов *.lib. Об этом все, наверное, знают, но про это не было упоминания в статье.
2.По поводу отложенной загрузки. К сожалению, как сказано в MSDN, такое подключение не позволяет импортировать данные: "Imports of data cannot be supported. A workaround is to explicitly handle the data import yourself using LoadLibrary (or GetModuleHandle after you know the delay-load helper has loaded the DLL) and GetProcAddress.".

- Sergey Kuryata

По поводу проблемы, описанной в конце статьи прошлого выпуска:

я не проделывал данных действий, но у меня всё слинковалось и заработало обычная линковка
#pragma comment(lib, "Delayimp.lib")
проходит, может потому, что установлен SP для MSVC 6.0

- Max Stepanov

/ / / В ПОИСКАХ ИСТИНЫ / / / / / / / / / / / / / / / /

Q| Возникла проблема... Существует sdi-приложение с CFormView-базированным видом.Существует несколько форм также основанных на CFormView.Необходимо динамически изменять основной вид на другие формы в процессе работы программы.Я так понимаю существует два пути.Первый - в OnCreate CMainFrame создавать все формы и потом сортировать их меняя z-порядок и второй - по мере необходимости создавать формы динамически.А вот с реализацией - :(.Или может я не прав? Заранее спасибо. - olegich

Ответить на вопрос

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Это все на сегодня. Пока!

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


No.24

Выпуск No. 24 от 19 ноября 2000 г.

Всем привет!

/ / / / СТАТЬЯ / / / / / / / / / / / / / / / / /

Как бороться с мерцанием

Вот наконец настал момент, когда работа над программой уже как бы закончена, все вылизано и подчищено, и шедевр вроде не глючит и даже заказчик кажется довольным. И все просто отлично... кроме одной мелочи - при изменении размеров окна элементы управления на форме сильно мерцают. Да, вроде бы мелочь. Да, многие коммерческие приложения тоже мерцают... даже ОЧЕНЬ многие.
Но все-таки от этого создается впечатление какой-то НЕИДЕАЛЬНОСТИ, недоделанности, что ли... И остается неприятный осадок в душе у вас (это еще полбеды!) - и у пользователей вашего приложения (а вот это намного серьезнее).

Windows в силу своего строения не позволит вам писать идеальные программы - даже если вы могли бы это делать - так как система сама далеко не идеальна.( Если кто знает идеальную - подскажите). В этой статье я хочу рассказать, как если уж не совсем убрать, то хотя бы значительно уменьшить такое мерцание, причем как это можно сделать буквально за несколько секунд ( Правда звучит совсем как реклама? Мы уже столько ее наслушались, что иногда и мыслим ею ;-)

Но сначала предлагаю вам разобраться, откуда вообще берется это мерцание, т.е. чем оно обусловлено.

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

Если все рисование происходит в одном окне (т.е. где нет контролов; помните, что любой контрол - это тоже окно) всегда можно избавиться от мерцания, если сначала выводить изображение в отдельный контекст устройства в памяти (memory device context), а затем одним махом переносить его на экран. (Этот принцип известен с доисторических времен и использовался еще в очень ранних компьютерных играх). Т.е. создаете в памяти совместимый с экраном контекст (CreateCompatibleDC), рисуете все в него, а затем вызываете BitBlt. О деталях реализации я распространяться не буду, т.к. они достаточно прозаичны. Как говаривал Laurence Fishbourne в "The Matrix", я могу показать вам дверь, а пройти через нее вы должны сами.

Другая ситуация встречается гораздо чаще - когда у вас есть дочерние окна (контролы). Типичным примером является любая форма или диалоговое окно. Главное окно - пустой прямоугольник, где выводятся разные кнопки, списки, строки редактирования... В этом случае, мерцание происходит когда Windows удаляет фон главного окна при вызове InvalidateWindow с fErase=TRUE. Система не удаляет фон сразу же, а ждет следующего цикла перерисовки - который наступает либо когда нечего больше делать, либо когда кто-то его форсировал с помощью UpdateWindow. В любом случае, предже чем посылать WM_PAINT, Windows вежливо просит окно очистить себя, посылая ему сообщение WM_ERASEBKGND. Стандартная процедура обработки сообщений (DefWindowProc) отвечает на это перерисовыванием окна цветом GetSysColor(COLOR_WINDOW+1), обычно белым. После того, как окно очистило фон, система посылает WM_PAINT и окно отрисовывает себя. (В случае формы/диалога, само окно ничего не рисует, а рисуют только дочерние окна. ) В результате получается мерцание: сначала вы видите, как окно целиком очищается, затем - как рисуются дочерние окна. Это мерцание особенно заметно при изменении размера окна, потому что система постоянно стирает и выводит заново все элементы управления. И чем их больше, тем сильнее мерцание.

Если вы специалист по програмированию в Windows, то можете подумать, что решение состоит в перехвате каким-либо образом сообщения WM_ERASEBKGND. Это хорошая идея, но есть способ проще. Один из стилей окна, который вы можете указать при его создании - WS_CLIPCHILDREN. Этот стиль сообщает Windows, что каждый раз, когда программа или сама система пытаются отобразить содержимое окна, области занятые дочерними окнами должны остаться нетронутыми. Так что все, что нужно сделать, чтобы значительно уменьшить мерцание контролов - указать родительскому окну стиль WS_CLIPCHILDREN. За это, конечно, вы платите незначительным уменьшением скорости, но все-таки это лучше, чем мерцание.

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

Cтатья основана на материалах C++ Q&A (MSDN)

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

Q| Возникла проблема... Существует sdi-приложение с CFormView-базированным видом.Существует несколько форм также основанных на CFormView.Необходимо динамически изменять основной вид на другие формы в процессе работы программы.Я так понимаю существует два пути.Первый - в OnCreate CMainFrame создавать все формы и потом сортировать их меняя z-порядок и второй - по мере необходимости создавать формы динамически.А вот с реализацией - :(.Или может я не прав? Заранее спасибо. - olegich

|A1 В начале немного теории.
В технологии Документ/Представление у документа (class CDocument) может быть несколько представлений (class CView и его наследники), которые в свою очередь находяться (отображаются) во Фреймах (CFrameWnd). Представление может отображаться только в одном Фрейме, но во Фрейме могут отображаться несколько представлений.

Как я понял вопрос был по поводу динамического переключения Представлений (т.е. класса CView и/или любого его наследника, которым и является класс CFormView) внутри одного Фрейма. Библиотека MFC прямого решения не предоставлет, но тем не менее такая возможность есть. MFC каждому представлению присваивает идентификатор от AFX_IDW_PANE_FIRST до AFX_IDW_PANE_LAST (всего 256 вчем легко убедиться посмотрев их значения) но на экран отображается только Представление с ID=AFX_IDW_PANE_FIRST. Поэтому задача сводиться к корретному изменению ID.

Теперь практика. Пусть имеется готовое SDI приложение (с технологией Документ/Представление). Создаем дополнительное Представление. Это делается в функции CFrameWnd::OnCreateClient примерно так:

BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext*
pContext)
{
// class CNewView - это наше новое представление
  pContext->m_pNewViewClass = RUNTIME_CLASS(CNewView);
// обратите внимание на идентификатор нового Представления
// переменная m_pNewView описанна в CMainFrame как CNewView* m_pNewView;
  m_pNewView = STATIC_DOWNCAST(CNewView, CreateView(pContext,
AFX_IDW_PANE_FIRST+1));
  m_pNewView->ShowWindow(SW_HIDE); // для сброса флага WS_VISIBLE

  return CFrameWnd::OnCreateClient(lpcs, pContext);
}

Теперь нам нужна функция переключения активного Представления на Представление только что нами созданное.

void CMainFrame::SwitchView(CView *pView)
{
  CFormView
    *pNewView=STATIC_DOWNCAST(CFormView, pView),
    *pActiveView=STATIC_DOWNCAST(CFormView, GetActiveView());
  if (pNewView!=NULL && pActiveView!=NULL)
  {
    UINT tempID=::GetWindowLong(pActiveView->GetSafeHwnd(), GWL_ID);
    ::SetWindowLong(pActiveView->GetSafeHwnd(), GWL_ID,
                ::GetWindowLong(pNewView->GetSafeHwnd(), GWL_ID));
    ::SetWindowLong(pNewView->GetSafeHwnd(), GWL_ID, tempID);

    pActiveView->ShowWindow(SW_HIDE);
    pNewView->ShowWindow(SW_SHOW);
    pNewView->OnInitialUpdate();
    SetActiveView(pNewView);
    RecalcLayout(); // можно делать можно и не делать
    pNewView->Invalidate();
  }
}
Похожий способ описан в Microsoft Knowledge Base (Q141334 - проект VSWAP32).

- Ilya Zharkov

|A2 Как и указывается в вопросе, эту задачу можно решать различными способами. Если виды переключаются часто, логичнее создать их один раз, а затем показывать один из них и скрывать все остальные. Если их переключают редко, имеет смысл создавать их динамически. Рассмотрим оба варианта.

Способ 1.
Сначала создаём все нужные нам виды. Поскольку в нормальном SDI-приложении вид, прописанный в шаблоне документа, создаётся в функции CFrameWnd::OnCreateClient, вполне естественно создать в ней и все остальные виды. Для этой цели удобно воспользоваться функцией CFrameWnd::CreateView, которая вызовет за нас и CView::CreateObject, и CView::Create с нужными параметрами. Всё, что требуется от нас - это подменить член m_pNewViewClass в структуре CCreateContext, указатель на которую передаётся в OnCreateClient. Выглядит это следующим образом.

BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext*
pContext)
{
   CRuntimeClass *ppFormViewClasses[] =
   {
      RUNTIME_CLASS(CFormView1),
      RUNTIME_CLASS(CFormView2)
      // :
   };

   for(int i=0; i < sizeof(ppFormViewClasses)/sizeof(CRuntimeClass *); i++)
   {
      pContext->m_pNewViewClass = ppFormViewClasses[i];
      if(!CreateView(pContext, AFX_IDW_PANE_FIRST+i+1))
         return FALSE;
   }

   SwitchView(AFX_IDW_PANE_FIRST+1);

   return TRUE;
}
Чтобы использовать этот фрагмент в реальной программе, нужно подставить правильные имена классов в массив ppFormViewClasses. Функция SwitchView добавляется в класс главного окна приложения и используется каждый раз, когда нужно переключиться с одного вида на другой. Она получает идентификатор нужного вида (в приведённом выше коде виды получают идентификаторы от AFX_IDW_PANE_FIRST+1 до AFX_IDW_PANE_FIRST+N, где N - число видов, но это поведение легко изменить на любое другое). Её код может выглядеть так:

void CMainFrame::SwitchView(UINT ID)
{
   if(ID == m_CurID)
      return;

   CView *pOldView;
   CView *pNewView;

   pOldView = GetActiveView();
   pNewView = (CView *)GetDlgItem(ID);

   if(pOldView)
   {
      pOldView->SetDlgCtrlID(m_CurID);
      pOldView->ShowWindow(SW_HIDE);
   }

   m_CurID = ID;
   pNewView->SetDlgCtrlID(AFX_IDW_PANE_FIRST);
   pNewView->ShowWindow(SW_SHOW);
   SetActiveView(pNewView);

   RecalcLayout();
}
Чтобы эта функция работала правильно, необходимо добавить переменную m_CurID типа UINT в класс главного окна. В ней временно хранится идентификатор текущего вида, поскольку при переключении он заменяется на AFX_IDW_PANE_FIRST. Это необходимо, так как функции класса CFrameWnd во многих местах предполагают, что идентификатор активного вида именно такой.

Способ 2.
Его реализация проще. Допустим, нам требуется создать для просмотра документа вид класса CSomeView. Для этого мы уничтожаем текущий вид, создаём новый и перерисовываем экран. Функцию, которая делает всё это, можно вставить в класс главного окна. Выглядит она так:

void CMainFrame::ReplaceView(CRuntimeClass *pRuntimeClass)
{
   CView *pActiveView = GetActiveView();
   if(pActiveView->IsKindOf(pRuntimeClass))
      return;

   CCreateContext context;
   context.m_pCurrentDoc = GetActiveView()->GetDocument();
   context.m_pNewViewClass = pRuntimeClass;
   context.m_pCurrentFrame = this;

   SetActiveView((CView *)CreateView(&context, AFX_IDW_PANE_FIRST));
   pActiveView->DestroyWindow();
   RecalcLayout();
}

Эта функция вызывается следующим образом:

ReplaceView(RUNTIME_CLASS(CSomeView));

Обратите внимание, что во втором случае функция OnInitialUpdate будет вызвана только для первого вида (который прописан в шаблоне документа). Если она выполняет важные действия и в других классах, можно добавить её вызов в функцию ReplaceView. В заключение отмечу, что я нигде не использовал тот факт, что виды порождаются от CFormView. Поэтому описанные мною способы годятся для любых других видов (например, порождённых от CScrollView или CListView).

- Alexander Shargin

/ / / В ПОИСКАХ ИСТИНЫ / / / / / / / /

Q| У меня вопрос для гуру. В конференциях от пару раз возникал, но как-то так тихо и кончался. То ли это очевидная истина, то ли никто не знает (чему я не верю). Итак вопрос: как в программу на правую кнопку подцепить меню, такое же как в Експлорере? Как туда напихать свои элементы? Второй второй вопрос отпадает, если можно выцепить именно меню, а не какую-то системную функцию, которая выводит окно меню, а тебе с этим сприходится смиряться, как с фактом бытия этого экранного элемента.... - Serg Loginov

Ответить на вопрос

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Это все на сегодня. Пока!

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


No.25

Выпуск No. 25 от 26 ноября 2000 г.

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

/ / / / СТАТЬЯ / / / / / / / / / / / / / / / /

Профилирование : анализ и оптимизация

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

Еще одна причина использования профилирования - это контроль за качеством тестов. Такой контроль предоставляет информацию о том, осуществляет ли данный тест вызов и обработку результатов выполнения всех без исключения подпрограмм, и какие строки кода при этом выполняются.

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

Итак, профилирование используется для того, чтобы определить:

  • Оптимален ли использованный алгоритм (по времени);
  • Слишком большое (или слишком малое) количество вызовов подпрограммы;
  • Покрывается ли участок кода тестирующими процедурами.

Приведу пример из жизни: в программе, осуществляющей доступ к данным из файла MS Access, наблюдалось катастрофичное падение производительности при превышении объема данных определенной величины, причем относительно небольшой (порядка 500 записей). Применение профилирования позволило мгновенно выяснить, что подпрограмма, извлекающая данные из базы, совершенно здесь не при чем, а всему виной подпрограмма, заносящая данные в таблицу на экране. После внесения соответствующих изменений, все отлично (т.е. БЫСТРО) заработало, причем скорость программы при просмотре записей практически перестала зависеть от объема данных.

Разделяют два вида профилирования: по функциям и по строкам кода.

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

  • суммарный объем времени, в течение которого выполнялась функция + количество вызовов этой функции (function timing),
  • только количество вызовов функции (function counting),
  • список ни разу не вызывавшихся функций (function coverage),
  • запись содержимого стека при каждом вызове функции (function attribution).

    Профилирование по строкам используется для проверки алгоритмов, т.к. позволяет посмотреть, сколько раз была выполнена каждая строчка, а также выявить строки, не выполнившиеся вообще ни разу. Здесь есть только два варианта: подсчет строк (line counting) - т.е. сколько раз данная строка была выполнена; и покрытие строк (line coverage) - показывает те строки, которые выполнялись хотя бы раз.

    Перейдем к практике. Если у вас установлен Visual C++ Professional или Enterprise Edition, то профилировщик у вас есть, он встроен в IDE. Осталось только научиться им пользоваться. Предугадывая поток писем, замечу, что существует довольно большой выбор всяческих профилировщиков от сторонних фирм, возможности которых иногда действительно впечатляют. Но в данной статье я кратко рассмотрю возможности профилирования, встроенные в Visual C++.

    Прежде всего необходимо установить опции проекта для включения профилирования (т.е. генерации профилировочной информации). Это делается через Project Settings|Link|Enable Profiling.

    Дальше выберите Build|Profile, и появится диалог "Profile", где можно выбрать любой тип профилирования, плюс еще есть возможность как следует это все настроить с помощью Custom Options (см. параметры команды PREP). Примечательна также опция Merge - позволяет совместить текущие результаты с предыдущими для наглядного сравнения. После нажатия на "OK" запускается ваша программа - дальше вы производите те действия, которые вам необходимо проверить. По завершении работы вашего приложения, профилировочная информация выводится в Profile Output Window, где вы ее анализируете... и делаете выводы.

    И напоследок, несколько советов.

    • Не стоит профилировать все приложение целиком, лучше сконцентрироваться на каких-то отдельных частях, представляющих наибольший интерес. (См. параметры /EXC и /INC). Есть много частей, которые просто нет смысла профилировать - такие, как пользовательский интерфейс, например.
    • Замеры не всегда будут точными, поэтому имеет смысл брать среднее значение от нескольких проходов. Собирать статистику за несколько проходов можно с помощью опции Merge.
    • Когда вы профилируете, постарайтесь, чтобы количество запущенных процессов в системе было минимальным.
    • Лучше отсоединиться от локальной сети или интернета, чтобы ОС не приходилось принимать входящие пакеты.
    • Следите за количеством вызовов. Например, у вас в алгоритме есть итерация на тысячу повторений - проследите, чтобы функции, которыми он пользуется, вызывались соответствущее число раз.
    • Время, в течение которого выполняется функция, включает также и время, в течение которого выполнялись все функции, вызванные из данной.

    Заинтересовавшимся данной темой предлагаю следующие статьи в MSDN:

  • Performance Tuning
  • Using Profile, PREP and PLIST
  • Profiling from the Development Environment

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

    Q| У меня вопрос для гуру. В конференциях от пару раз возникал, но как-то так тихо и кончался. То ли это очевидная истина, то ли никто не знает (чему я не верю). Итак вопрос: как в программу на правую кнопку подцепить меню, такое же как в Експлорере? Как туда напихать свои элементы? Второй второй вопрос отпадает, если можно выцепить именно меню, а не какую-то системную функцию, которая выводит окно меню, а тебе с этим сприходится смиряться, как с фактом бытия этого экранного элемента.... - Serg Loginov

    |A1 Контекстное (правокнопочное) меню создать довольно просто:
    1. Добавляем в графическом редакторе новое пустое меню.
    2. Для крайнего слева элемента верхнего уровня вводим какое-нить имя и в полученное раскрывающееся меню добавляем команды.
    3. Вставляем обработчик сообщения WM_CONTEXTMENU в класс "вид" или в класс другого окна, получающего сообщения от кнопок мыши, ну, например, в CMyDialog. Обработчик этот программируем так:

    CMyDialog :: OnContextMenu ( CWnd* pWnd, CPoint point ) { CMenu menu; menu.LoadMenu ( IDR_MYMENU ); menu.GetSubMenu ( 0 ) -> TrackPopupMenu ( TPM_LEFTALIGN | TPM_RIGHTBUTTON , point.x , point.y , this ); }

    Вот и все. TrackPopupMenu и занимается выводом контекстного меню на экран. Правда, объект класса CMenu лучше сделать мембером класса, тогда в любой функции можно будет удалять, добавлять, запрещать etc. элементы меню. Конечно, в этом случае m_Menu.LoadMenu ( IDR_MYMENU ); надо написать в OnInitDialog. Заметте, OnContextMenu получает координаты курсора, т.е. можем для разных областей окна элементарно выводить разные меню, просто проверяя координаты.

    - Sergey Pochechuev

    |A2 В Windows все файлы и папки входят в иерархию объектов оболочки. В неё также входят и объекты, не имеющие отношение к файловой системе: корзина, рабочий стол и т. п. Windows Explorer является по сути программой для просмотра этой иерархии объектов.

    Каждый объект оболочки обязан реализовывать COM-интерфейс IShellFolder. Многие объекты реализуют и ряд других интерфейсов. Так, IExtractIcon отвечает за иконку объекта, а IContextMenu - за его контекстное меню. Эксплорер использует эти (и другие) интерфейсы, чтобы корректно отображать элементы иерархии объектов и позволять пользователю манипулировать ими. Мы также можем воспользоваться этими интерфейсами.

    Итак, нам требуется функция, которая выводила бы контекстное меню для заданного файла (каталога), который передавался бы ей в качестве параметра. Эта функция должна выполнить следующие действия:

    • Получить интерфейс IContextMenu для этого файла (каталога).
    • Создать всплывающее меню (посредством CreatePopupMenu).
    • Заполнить его элементами с помощью IContextMenu::QueryContextMenu.
    • Показать меню пользователю (TrackPopupMenu).
    • Выполнить выбранную команду посредством IContextMenu::InvokeCommand

    Основную сложность на самом деле представляет первый этап. Получить указатель на IContextMenu мы можем только, имея указатель на базовый интерфейс IShellFolder, но Windows не предоставляет простого способа получить этот указатель. Выполнение этой задачи в свою очередь распадается на несколько шагов:

    - Получить интерфейс IShellFolder рабочего стола посредством SHGetDesktopFolder.
    - Построить LPITEMIDLIST для заданного файла (каталога), используя IShellFolder::ParseDisplayName.
    - Получить IShellFolder для этого файла вызовом IShellFolder::BindToObject

    Функция, которая отображает контекстное меню, может выглядеть примерно так (я снабдил её подробными комментариями).

    void ShowContextMenu (CWnd *pWnd, LPCTSTR pszPath, CPoint point)
{
   // Строим полное имя.
   TCHAR tchPath[MAX_PATH];
   GetFullPathName(pszPath, sizeof(tchPath)/sizeof(TCHAR), tchPath, NULL);

   // Если нужно, перекодируем ANSI в UNICODE.
   WCHAR wchPath[MAX_PATH];
   if(IsTextUnicode (tchPath, lstrlen (tchPath), NULL))
      lstrcpy ((char *) wchPath, tchPath);
   else
      MultiByteToWideChar(CP_ACP, 0, pszPath, -1, wchPath,
                          sizeof(wchPath)/sizeof(WCHAR));

   // Получаем интерфейс IShellFolder рабочего стола
   IShellFolder *pDesktopFolder;
   SHGetDesktopFolder(&pDesktopFolder);

   // Преобразуем путь в LPITEMIDLIST
   LPITEMIDLIST pidl;
   pDesktopFolder->ParseDisplayName(pWnd->m_hWnd, NULL, wchPath, NULL, &pidl, NULL);

   // Получаем интерфейс IShellFolder для заданного файла (папки)
   IShellFolder *pFolder;
   pDesktopFolder->BindToObject(pidl, NULL, IID_IShellFolder, (void**)&pFolder);

   // Получаем интерфейс IContextMenu для заданного файла (папки)
   IContextMenu *pContextMenu;
   pFolder->GetUIObjectOf(
      pWnd->m_hWnd, 1, (LPCITEMIDLIST *)&pidl, IID_IContextMenu, NULL, (void**)&pContextMenu);

   // Создаём меню
   CMenu PopupMenu;
   PopupMenu.CreatePopupMenu();

   // Заполняем меню
   pContextMenu->QueryContextMenu(PopupMenu.m_hMenu, 0, 1, 0x7FFF,CMF_EXPLORE);

   // Отображаем меню
   UINT nCmd = PopupMenu.TrackPopupMenu(
      TPM_LEFTALIGN|TPM_LEFTBUTTON|TPM_RIGHTBUTTON|TPM_RETURNCMD,
      point.x, point.y, pWnd);

   // Выполняем команду (если она была выбрана)
   if(nCmd)
   {
      CMINVOKECOMMANDINFO ici;
      ZeroMemory(&ici, sizeof(CMINVOKECOMMANDINFO));
      ici.cbSize = sizeof(CMINVOKECOMMANDINFO);

      ici.hwnd = pWnd->m_hWnd;
      ici.lpVerb = MAKEINTRESOURCE(nCmd-1);
      ici.nShow = SW_SHOWNORMAL;

      pContextMenu->InvokeCommand(&ici);
   }

   // Получаем интерфейс IMalloc.
   IMalloc *pMalloc;
   SHGetMalloc(&pMalloc);

   // Используем его для освобождения памяти, выделенной на ITEMIDLIST
   pMalloc->Free(pidl);

   // Освобождаем все полученные интерфейсы
   pDesktopFolder->Release();
   pFolder->Release();
   pContextMenu->Release();
   pMalloc->Release();

   return;
}

    Эту функцию можно вызывать, например, из обработчика OnContextMenu. Делается это так:

    void CMyView::OnContextMenu(CWnd* pWnd, CPoint point) { ShowContextMenu (pWnd, "C:\\command.com", point); }

    За дополнительной информацией следует обратиться к следующим статьям в MSDN:
    - Periodicals 1997, Microsoft Systems Journal, April, Wicked Code
    - Knowledge Base, статья ID: Q198288
    - Описание IShellFolder и IContextMenu

    Что касается второго вопроса (о создании собственных пунктов меню), мы имеем полный контроль над процессом создания меню, а значит можем делать с ним всё, что угодно. Нужно только иметь в виду 2 момента.
    Во-первых, поскольку функция TrackPopupMenu вызывается с флагом TPM_RETURNCMD, она на будет отправлять окну сообщение WM_COMMAND. Поэтому нужно анализировать значение nCmd, возвращённое функцией TrackPopupMenu и вызывать нужный обработчик вручную. Например:

UINT nCmd = PopupMenu.TrackPopupMenu(...);

if(nCmd)
{
   if(nCmd == 0x8000)
   {
      AfxMessageBox("It works!!!");
   }
   else
   {
      // Используем IContextMenu::InvokeCommand
   }
}
    Во-вторых, функция IContextMenu::QueryContextMenu получает параметры idCmdFirst, idCmdLast (в примере выше они равны 1 и 0x7FFF соответственно). Идентификаторы для стандартных пунктов меню выбираются именно в диапазоне от idCmdFirst до idCmdLast. Поэтому нужно проследить, чтобы идентификаторы пользовательских пунктов меню в этот диапазон не попали.

    - Alexander Shargin

    / / / / ОБРАТНАЯ СВЯЗЬ / / / / / / / / /

    Когда начал читать вашу статью на тему мерцания, подумал было, что вы обязательно упомянете тот метод, который использовал я в своей программе. На мой взгляд, он достаточно известен, и, кажется, является самым самым лучшим.
    Нужно просто создать обработчик события WM_ERASEBKGND с одной-единственной строчкой:

    BOOL CSomeClass::OnEraseBkgnd(CDC* pDC)
    {
    return FALSE;
    }
    Т. е., по-русски говоря, программа фон не очистила, рисуйтесь полностью.

    - Vlad

    На ответ A1 из прошлого выпуска:
    Теперь практика. Пусть имеется готовое SDI приложение (с технологией Документ/Представление). Создаем дополнительное Представление. Это делается в функции CFrameWnd::OnCreateClient примерно так:

BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext*
pContext)
{
// class CNewView - это наше новое представление
  pContext->m_pNewViewClass = RUNTIME_CLASS(CNewView);
// обратите внимание на идентификатор нового Представления
// переменная m_pNewView описанна в CMainFrame как CNewView* m_pNewView;
  m_pNewView = STATIC_DOWNCAST(CNewView, CreateView(pContext,
AFX_IDW_PANE_FIRST+1));
  m_pNewView->ShowWindow(SW_HIDE); // для сброса флага WS_VISIBLE

  return CFrameWnd::OnCreateClient(lpcs, pContext);
}
    Этот код работает неправильно, причём это видно даже невооружённым взглядом. В последней строчке функции CMainFrame::OnCreateClient вызывается функция базового класса. Но ведь поле pContext->m_pNewViewClass уже изменилось! В результате вместо двух разных видов будет создано два одинаковых. Ошибка лечится переносом вызова функции из базового класса в начало переопределённой функции:

BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext*
pContext)
{
  int nResult = CFrameWnd::OnCreateClient(lpcs, pContext);
  ...
  return nResult;
}
    Кроме того, неясно, как использовать функцию SwitchView. Указатель на созданный нами вид хранится в m_pNewView, но для получения указателя на вид, созданный самой MFC, не видно удобного способа. Вероятно, лучший вариант - также сохранить его в члене класса CMainFrame.

    - Alexander Shargin

    / / / В ПОИСКАХ ИСТИНЫ / / / / / / / / / /

    Q| У меня dialog-base приложение, живет в systray. Необходимо, чтобы приложение при повторном запуске находило уже запущеный экземпляр программы и активизировало его. Я пытался сделать это через FindWindow(), в которую передается имя зарегистрированного класса окна, и заголовок окна, которое разыскивается. По заголовку я искать не могу, так как он все время у меня меняется. Следовательно, нужно искать по зарегистрированному имени класса окна. Вот тут то и начинается проблема. Я его не знаю. MFC сама их раздает dialog-based приложениям. А переопределить это имя можно было бы в PreCreateWindow(), но этот метод CDialog не наследует из CWnd. Во всех остальных методах, имя класса уже зарегистрированно, т.е. менять его поздно. Как быть? - el-f

    Ответить на вопрос

    / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

    Это все на сегодня. До новых встреч!

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


No.26

Выпуск No. 26 от 3 декабря 2000 г.

Здравствуйте!

Итак, вот уж и зима на дворе... Время, когда отходить от компьютера не хочется даже ненадолго ;) Правда, это если у вас в комнате достаточно тепло. В другом случае не хочется вылезать из-под трех одеял;)

/ / / / IDE / / / / / / / / / / / / / / / /

В прошлом выпуске мы с вами говорили о профилировании программ. Некоторые читатели просили также рассказать обо всех богатых возможностях отладки, которые предлагает Visual C++. Александр Шаргин, наш постоянный автор, любезно предложил написать в рассылку статью на эту тему. Думаю, что даже умеющие пользоваться отладчиком программисты найдут в ней для себя много интересного.

Использование отладчика в Visual C++

В этой статье я очень кратко расскажу о возможностях встроенного отладчика Visual C++.

Запуск отладки
Чтобы запустить программу на отладку, нужно выбрать одну из команд меню Build->Start Debug. Обратите внимание, что команда Attach to Process позволяет подключиться к уже запущенному процессу.

Точки останова (Breakpoints)
Точки останова служат для прерывания программы, выполняемой в отладчике. Их можно привязывать к конкретной строке в коде программы, к переменной или к сообщению Windows. После того как программа прервана, её можно выполнять в пошаговом режиме или просто проанализировать значения переменных, после чего продолжить выполнение.

Чтобы установить точку останова на строку вашей программы, достаточно выбрать команду Insert/Remove Breakpoint или нажать F9. Однако гораздо больше возможностей предоставляет окно Breakpoints из меню Edit. В нижней части этого окна находится список уже поставленных точек останова (любую из них можно активизировать, отключить или удалить), а вверху расположены три вкладки, предназначенные для установки точек останова различных типов.

Вкладка Location
Здесь устанавливаются точки останова, привязанные к конкретным строкам в вашей программе. Адрес точки останова задаётся в поле Break at в виде {имя_функции, имя_файла_cpp, имя_файла_exe} @номер_строки

Для формирования адреса можно воспользоваться окном Advanced breakpoint; чтобы вызвать это окно, щелкните на стрелке справа от поля ввода и выберите пункт Advanced. Обычно достаточно задать только номер строки и имя файла с исходным кодом.

В окне Condition можно дополнительно указать условие срабатывания точки останова. Условием может быть любое выражение. Если заданное вами выражение имеет тип bool, точка останова срабатывает, когда оно истинно; в противном случае она срабатывает при изменении значения выражения.

Бывают случаи, когда точку останова нужно пропустить несколько раз, прежде чем прерывать на ней программу. Специально для этого в окне Condition предусмотрено ещё одно поле Skip count (в самом низу). С помощью этого поля можно, к примеру, пропустить 10 итераций цикла и прервать программу только на одиннадцатой.

Вкладка Data
На этой вкладке устанавливаются точки останова по данным. Их отличие состоит в том, что они могут сработать в любом месте программы, как только изменится (или станет истинным) введённое вами выражение.

Если выражение имеет смысл только в определённом контексте (например, в нём используются локальные переменные какой-либо функции), этот контекст необходимо указать с помощью всё того же окна Advanced breakpoint, но здесь уже важно указать имя функции, а не файла.

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

LRESULT WINAPI WndProc(HWND, UINT, WPARAM, LPARAM);

В программах, использующих MFC, удобнее ставить точки останова в соответствующие обработчики сообщений.

Пошаговая отладка
После того, как программа прервана, её можно выполнять в пошаговом режиме. Для этого в отладчике предусмотрены следующие команды (из меню Debug).

Go (F5) - продолжить выполнение программы до следующей точки останова.
Step Into (F11) - выполнить одну инструкцию; если это вызов функции, точка выполнения перемещается на первую инструкцию этой функции.
Step Over (F10) - выполнить одну инструкцию; если это вызов функции, то она выполняется целиком.
Step Out (Shift+F11) - выполнять программу до возврата из текущей функции.
Run to Cursor (Ctrl+F10) - эквивалентна установке временной точки останова с последующим вызовом команды Go.

Иногда в процессе отладки возникает необходимость перенести точку выполнения. Например, вы заметили ошибку и хотите "перескочить" через неё или, наоборот, хотите вернуться немного назад и выполнить фрагмент программы ещё раз. Чтобы это сделать, установите курсор в нужном месте и выберите команду Set Next Statement из контекстного меню (или нажмите Ctrl+Shift+F10).

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

Окно Variables
В этом окне автоматически отображаются значения локальных переменных (вкладка Locals), переменных-членов класса, адресуемого указателем this (вкладка This), а также всех переменных, которые используются в предыдущей и текущей строках программы (вкладка Auto). На вкладке Auto также показываются возвращаемые значения функций.

Чтобы изменить значение переменной в окне Variables, достаточно просто два раза кликнуть на старом значении и ввести новое.

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

Вы, вероятно, заметили, что отладчик "умеет" распознавать стандартные структуры данных (CString, RECT и т. п.) и показывать их содержимое в удобном виде. Оказывается, можно не только изменить представление этих структур в окне Variables, но и определить представление для собственных структур. Для этого нужно отредактировать файл autoexp.dat, расположенный в каталоге
:\Common\MSDev98\Bin. Описание формата приводится в самом файле.

Окно Watch
Окно Watch позволяет просматривать значения переменных и выражений. Переменные и выражения можно размещать на любой из четырёх вкладок. Добавить переменную или выражение в окно Watch можно одним из следующих способов:
- Ввести с клавиатуры
- Перетащить из окна редактора или из окна Variables
- Добавить из окна Quick watch

Чтобы изменить значение переменной, достаточно два раза кликнуть на старом значении и ввести новое. Значение выражений изменять нельзя.

Чтобы узнать тип переменной или выражения, нужно щёлкнуть по ним правой кнопкой и выбрать Properties из всплывающего меню.

В окне Watch можно наблюдать любые регистры процессора и изменять их значения, хотя это удобнее делать в окне Registers. Можно также использовать регистры в выражениях.

Можно указать отладчику, в каком формате выводить значение переменной/выражения, используя флаги форматирования. Эти флаги добавляются к имени переменной или выражению через запятую. Большинство из них совпадает с символами форматирования функции printf: d - целое число со знаком, u - беззнаковое целое, f - число с плавающей точкой, c - символ, s - строка и т. д. Однако есть четыре флага, на которых я хочу остановиться подробнее.

Флаг wm превращает код сообщения в его название, например:
0x01,wm = WM_CREATE

Флаг wc позволяет <расшифровать> стиль окна, например:
0x6840000,wc = WS_OVERLAPPEDWINDOW WS_CLIPSIBLINGS WS_CLIPCHILDREN

Флаг hr переводит коды ошибок Win32 и значения HRESULT, возвращаемые функциями COM, в удобочитаемый вид, например:
0x02,hr = 0x00000002 Системе не удается найти указанный файл.

Наконец, в Visual C++ есть числовой флаг, который позволяет просмотреть заданное количество элементов массива, адресуемого указателем (по умолчанию показывается всего один элемент). Допустим, мы выделили динамический массив из 10 целых чисел:
Int *pInt = new[10];

Чтобы просмотреть его содержимое в окне Watch, нужно ввести:
pInt,10

Псевдорегистр ERR
Как известно, получить расширенный код ошибки после вызова функций Win32 API можно с помощью GetLastError. Однако расставлять по всей программе вызовы GetLastError крайне неудобно. Поэтому в отладчике Visual C++ предусмотрен специальный псевдорегистр ERR, который всегда содержит расширенный код ошибки. Особенно удобно наблюдать значение этого регистра, использую уже знакомый нам флаг hr. Добавьте ERR,hr в окно Watch, и информация об ошибках в вызовах функций API всегда будет у вас перед глазами.

Другие окна отладчика
Окно Registers. Позволяет просматривать и изменять значения регистров процессора.
Окно Memory. Позволяет просматривать и изменять содержимое ячеек памяти.
Окно Call Stack. Показывает последовательность вызванных функций. Используя контекстное меню, можно отобразить также типы и значения параметров этих функций. К тексту любой функции можно переместиться, сделав двойной щелчок на её имени. Обратите внимание, что точки останова можно ставить прямо в этом окне.
Окно Disassembly. Показывает текст отлаживаемой программы на языке ассемблера. Иногда без помощи этого окна ошибку в программе найти не удаётся.

Диалоги
Диалоги отладчика предоставляют вам ряд дополнительных возможностей. Они вызываются из меню Debug.

Quick Watch. Имеет возможности, аналогичные возможностям окна Watch, с той разницей, что в нём можно просматривать только одну переменную за раз. Используется, когда вам не хочется добавлять переменную в окно Watch.
Exceptions. Позволяет настроить реакцию отладчика на возникновение системных и пользовательских исключений.
Threads. Показывает список активных потоков. Позволяет приостановить (suspend) или продолжить (resume) поток, а также установить на него фокус.
Modules. Показывает список загруженных модулей. Для каждого модуля выводится диапазон адресов и имя файла.

Edit and Continue
В заключение хотелось бы упомянуть о новой мощной возможности, которая появилась в Visual C++ 6.0 - Edit and Continue. С её помощью вы можете вносить изменения в код программы и перестраивать её, не прерывая сеанса отладки.

Для этого достаточно вызвать команду Apply code changes из меню Debug (или нажать Alt+F10), после того как вы подправили исходные тексты. Более того, Visual C++ может вызывать для вас эту команду автоматически. Это будет происходить, если в окне Tools->Options на вкладке Debug установить флаг Debug commands invoke Edit and Continue.

- Александр Шаргин

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

Q| У меня dialog-base приложение, живет в systray. Необходимо, чтобы приложение при повторном запуске находило уже запущеный экземпляр программы и активизировало его. Я пытался сделать это через FindWindow(), в которую передается имя зарегистрированного класса окна, и заголовок окна, которое разыскивается. По заголовку я искать не могу, так как он все время у меня меняется. Следовательно, нужно искать по зарегистрированному имени класса окна. Вот тут то и начинается проблема. Я его не знаю. MFC сама их раздает dialog-based приложениям. А переопределить это имя можно было бы в PreCreateWindow(), но этот метод CDialog не наследует из CWnd. Во всех остальных методах, имя класса уже зарегистрированно, т.е. менять его поздно. Как быть? - el-f

На этот вопрос пришло просто огромное количество ответов, и разных. Отдельное спасибо хочу сказать Андрею Твердохлебову, который прислал ссылку на действительно замечательную статью на эту тему, которая позволила многое прояснить. Именно в ней приводится по-настоящему правильный и надежный способ.

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

|A1 Относительно вопроса, заданного в No.25 данной рассылки хотелось бы сразу высказать сильное сомнение по поводу возможности его решения при помощи использования имен оконных классов. Мне кажется, что каждый экземпляр приложения в операционной системе Windows имеет, как это не пародоксально звучит, свой набор зарегистрированных оконных классов. Общность стандартных (системных) оконных классов поддерживается автоматической загрузкой в адресное пространство системных библиотек, которые, в момент своей инициализации, регистрируют свои оконные классы. Список DLL (только они позволяют делать общедоступными определенные виды окон), подгружаемых автоматом каждому приложению, хранится где-то в реестре (не помню точно где). Еще одним доводом в пользу предположения об уникальности списка зарегистрированных оконных классов каждого приложения (экземпляра приложения) служит сама процедура RegisterClass(Ex). В качестве аргумента данной процедуры выступает указатель на структуру, одним из элементов которой является указатель (адрес) на оконную процедуру. Нет 100% гарантии того, что разные DLL проекцируются в одно и то же адресное пространство всех приложений без исключений. Следовательно, адрес оконной процедуры перестает нести смысловую нагрузку. Из изложенного выше, позвольте сделать вывод: если в результате регистрации приложением оконного класса произошла ошибка типа "Оконный класс с указанным именем уже существует" это означает лишь то, что именно ЭТО приложение уже зарегистрировало подобный класс. И наоборот, если регистрация прошла успешно, то это не значит, что нет такого приложения (экземпляра приложения) которое не зарегистрировало бы оконный класс с подобным именем. Следовательно подобный подход при решении подобной проблемы невозможен.

Решить указанную проблему можно лишь при помощи объектов ядра операционной системы, объектов файловой системы (так, по моему, решаются подобные задачи в OS типа Linux/Unix) и некоторых других объектов (типа mailslot, TCP ports и наверное можно придумать что либо еще). Важно выполнение следующих условий:
1. Объекты должны быть доступны из различных приложений
2. Объекты должны быть одинаковым образом идентефицированы для всех приложений (идет отказ от дескрипторов объектов, которые актуальны только в рамках одного процесса).
3. Желательно (а может обязательно?), что бы OS поддерживала синхронизацию доступа к данным объектам.
Самый простой способ идентефикации объектов заключается в присвоении им строковых имен. Такой способ применяестся к объектам ядра, файловой системы, mailslot. TCP способ использует разименовку по номеру порта. И имя и номер имеют одно и то же значение для всех приложений. Для функции RegisterClass(Ex), похоже, не выполняется первое условие. Способ определения повторного запуска экземпляра приложения давно известен и использует объект ядра системы типа "mutex". В задаче, кроме того, требуется подать сигнал активизации первому экземпляру приложения. Пришлось модифицировать известный способ, попутно решив эту проблему для себя, и использовать объект ядра типа "event". В общем, принцип работы схемы выглядит так:

1. Попытка получить доступ к объекту ядра по имени
1.1. Доступ получить не удалось из-за отстутствия объекта с указанным именем - данный экземпляр приложения первый. Переходим к п. 2
1.2. Доступ получен - большая вероятность того что данное приложение запущено во второй раз. Почему не 100% уверенность? Делаем скидку на то, что кто то другой мог выбрать для своих нужд именно этот тип объектов и именно с этим именем :). Переходим на п. 3
2. Инициализация и активация главного окна
2.1. Создаем объект ядра с именем, использованным в п. 1
2.2. Инициализируем и запускаем приложение
2.3. Время от времени проверяем поступление сигнала о запуске второй копии приложения
3. Передача сигнала первому приложению о запуске второй копии и выход из программы.

- Владимир Голенкевич

Очень подробный и интересный ответ, но к сожалению не совсем корректный. Насчет классов - действительно, они доступны только внутри зарегистрировавшего их процесса (в MSDN есть хорошая статья "Window Classes in Win32" by Kyle Marsh). Однако я не совсем согласен с логическими построениями автора (или понял их неправильно). Т.к. имя класса по идее уникально, то естественно, что ИМЕННО ЭТО приложение зарегистрировало класс. А как иначе?..

Вопросы вызывают еще два момента. Во первых, при создании объектов ядра НИ В КОЕМ СЛУЧАЕ не нужно сначала проверять, существует ли такой объект (п.1). Иначе можно нарваться на т.н. race conditions, описанные в вышеупомянутой статье - ситуация, когда два экземпляра стартуют почти одновременно. Получается, что одна копия проверяет, что объект не существует, и создает его. Но прежде чем она его создаст, вторая копия тоже убеждается в том, что объекта еще нет, и тоже собирается со спокойной совестью его создать. В результате первая копия успевает создать объект, а второй копии создать объект так и не удается, но это уже не важно, т.к. вторая копия все равно запускается.
Не стоит думать, что такая ситуация маловероятна. Представьте себе пользователя, который настроил Windows запускать программы по одному щелчку на ярлыке, но по привычке делает double-click...
Весь смысл объектов ядра как раз в том, что при их создании ГАРАНТИРУЕТСЯ, что никто другой в это же время не сможет создать такой же объект. Нужно сразу пытаться СОЗДАТЬ объект - и если эта операция не удается (возвращается ERROR_ALREADY_EXISTS или ERROR_ACCESS_DENIED) - вот тогда можно с уверенностью говорить о том, что запущена еще одна копия.

Во-вторых, мне не совсем понятны пункты 2.3 и 3. Мне кажется это очень неэфективным - постоянно проверять на наличие сигнала от второй копии (как я понимаю, по этому сигналу приложение должно себя активизировать). Есть способы гораздо лучше (читайте ниже).

Но (заметьте!) мы выяснили очень важную вещь: использование объектов ядра абсолютно необходимо для определения, запущена ли копия приложения или нет.

|A2 Можно например с помощью RegisterWindowMessage.
В двух словах:
1. Регистрируем сообщение.
2. Отправляем его на HWND_BROADCAST с каким нибудь кодом в wParam. (например 1) и своим hWnd в lParam (чтобы получатель знал, куда отправлять ответ)
3. Пишем обработчик нашего зарегистрированного сообщения. Он анализирует wParam, если там 1 и lParam не равен собственному hWnd, то он отсылает в ответ такое же сообщение но с кодом 2 например.(отправителя мы получили через lParam)
4. Если мы получили сообщение с кодом 2 в wParam значит уже есть запущенная копия приложения.

- Pavlik Yatsuk

Если к ответу добавить механизм объектов ядра, то получается вариант правильный.. на первый взгляд. Вот что говорит об этом способе Александр Шаргин:

"Я отказался от этого подхода, и вот почему [...] Посылая сообщение с параметром HWND_BROADCAST, мы теряем доступ к возвращаемому в ответ значению. А значит, уже запущенная копия нашего приложения (если таковая есть) должна ответить также посылкой сообщения. Вопрос: кому его посылать? Главное окно во второй копии приложения ещё не создано, цикла сообщений нет... Выход один: создавать невидимое окно, и ловить в нём сообщение- кривовато... Вариант второй: не использовать HWND_BROADCAST, а сделать EnumWindows и посылать сообщение каждому окну в отдельности. А значит писать свою CALLBACK-функцию, обработчик зарегистрированного сообщения... Тоже кривовато, мне не понравилось."

(Кстати, вариант второй как раз используется в статье;) А вот и сам его ответ:

|A3 Для начала два замечания. Во-первых, CDialog таки наследует функцию PreCreateWindow от своего предка - класса CWnd. Другой вопрос, что эта функция не вызывается в процессе создания диалогового окна. Во-вторых, MFC не регистрирует класс диалогового окна, оставляя имя, предопределённое в Windows. Вместо этого MFC передаёт адрес своей собственной диалоговой функции (AfxDlgProc) при вызове CreateDialogIndirect. Итак, мы установили, что диалоговое окно создаётся в функции CreateDialogIndirect. Мы не можем повлиять на процесс создания окна, а значит не можем и изменить имя класса. Придётся искать обходные пути. Самый простой из них, на мой взгляд - дать диалогу "во владение" невидимое окно, для которого заголовок и имя класса известны. Затем можно найти это окно с помощью FindWindow, переместиться к самому диалогу через GetWindow и сделать на него SetForegroundWindow. Вот фрагмент функции InitInstance, который делает всё необходимое (использование статических переменных выглядит несколько коряво - я использовал их, чтобы весь код был в одном месте, но в реальной программе лучше сделать их членами класса).

BOOL CMyApp::InitInstance()
{
    ...

    HWND hWnd = FindWindow( "{4C1D4220-C3E5-11d4-93A8-B5D00D46136A}", NULL);

    if(hWnd != NULL)
    {
        hWnd = GetWindow(hWnd, GW_OWNER);
        SetForegroundWindow(hWnd);
        return FALSE;
    }

    WNDCLASS wc;
    ZeroMemory(&wc, sizeof(wc));
    wc.hInstance = AfxGetInstanceHandle();
    wc.lpfnWndProc = DefWindowProc;
    wc.lpszClassName = "{4C1D4220-C3E5-11d4-93A8-B5D00D46136A}";
    RegisterClass(&wc);

    static CMyDlg dlg;
    m_pMainWnd = &dlg;
    dlg.Create(IDD_MY_DIALOG, NULL);

    static CWnd wndDummy;
    wndDummy.CreateEx(0,
                         "{4C1D4220-C3E5-11d4-93A8-B5D00D46136A}",
                         "",0, CRect(0,0,0,0), &dlg, 0);
    ...
    return TRUE;
}

Обратите внимание на использование GUIDа в качестве имени класса. Он получен с помощью утилиты Guidgen (меню Tools). Вероятность того, что в системе найдутся окна с таким классом, не имеющие отношения к нашей программе, представляется ничтожно малой.

- Александр Шаргин

А вот если к этому ответу добавить механизм mutex'ов, то получится дейтсвительно корректный способ.

Хочу обратить ваше внимание на один факт, присутствующий в обоих предыдущих ответах. Функция активизации уже запущенной копии целиком возлагается именно на вторую копию. Многие предлагали посылать первой копии сообщение, чтобы она воостановилась сама. Это в общем случае не работает (т.е. работает не во всех системах), из-за того, что приложение не может активизировать свое главное окно, если само не активно, и при этом не помогают ни BringWindowToTop, ни SetForegroundWindow.

Интересующимся этой темой я настоятельно рекомендую ознакомиться со статьей by Joseph M. Newcomer, где подробно разбираются достоинства и, главное, недостатки, каждого метода. А методов, помимо рассмотренных выше, очень много, напр. file mapping, shared variable и др. (я не стал публиковать эти ответы т.к. все эти объекты используются с одной целью, которая отлично решается с помощью mutex'ов).

Некоторые ссылались на статью в MSDN Q109175 - так вот: там используется некорректное решение!

Если вдруг кто-нибудь, кто прислал мне ответ, все еще считает его 100% правильным, прошу написать мне об этом - я никого обидеть не хотел, а в таком большом количестве ответов было легко что-то упустить. И еще: у кого есть какие соображения по этому поводу, замечания - пишите! Дискуссия получается на редкость интересная.

/ / / В ПОИСКАХ ИСТИНЫ / / / / / / / / / /

Q| Есть диалог на нем Date Time Picker и есть соответствующая ему переменная m_Time типа CTime. Проблема в том, что если m_Time = 0, то в диалоге высвечивается 2:00:00 !!?? Т.е. сдвиг на два часа. Причем если выставить 0:00:00, то будет "Assertion fault". Ну и соответственно, если установить 2:00:00, то после UpdateData() m_Time станет = 0. Скорее всего это как-то связано с часовым поясом (у меня часовой пояс +02:00). Как от этого избавиться? - Михаил

Ответить на вопрос

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

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

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


No.27

Выпуск No. 27 от 10 декабря 2000 г.

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

Я получал достаточно много писем с просьбами рассказать о чем-то конкретном , и в этих просьбах довольно часто встречалась тема доступа к данным из программ с использованием различных технологий - ODBC, DAO, OLE DB. Конечно, тема эта очень обширна и многогранна. Но, тем не менее, программистам с ней приходится сталкиваться довольно часто, и поэтому рассмотрение ее в рассылке кажется оправданным. Я решил, что разумнее всего будет сделать серию статей на эту тему, отдельные заметки из этой серии будут по мере написания появляться в рассылке (но, заметьте, что далеко не в каждом выпуске).

Сейчас я работаю над продолжением статьи про многозадачность. Тема синхронизации потоков думаю будет особенно интересна в свете того обсуждения, которое вызвал вопрос из выпуска No.25 (про активизацию уже запущенного экземпляра приложения в случае попытки запуска нового). В дальнейшем нас также ждет очень интересная тема о работе с e-mail.

Когда Александр Шаргин попросил меня перечислить вопросы, интересующие читателей рассылки, то я назвал ему и вопрос доступа к данным. К сегодняшнему дню он закончил работу над первой частью статьи про ODBC: технологии, с которой воистину все начиналось. Думаю, с нее стоит начать и нам.

/ / / / СТАТЬЯ / / / / / / / / / / / / / / / /

Доступ к БД с использованием ODBC
 Часть 1

Открытый интерфейс доступа к базам данных (Open Database Connectivity, ODBC) - это программный интерфейс, который позволяет приложению обращаться к различным СУБД, используя структурированный язык запросов SQL. Применяя ODBC, разработчики могут писать программы, независимые от архитектуры конкретной СУБД. Такие программы будут работать с любой реляционной базой данных (как существующей в данный момент, так и той, которая, возможно, появится в будущем), для которой написан ODBC-драйвер.

НЕМНОГО ТЕОРИИ

Структура ODBC
Архитектура ODBC имеет четыре основных компонента: пользовательское приложение, менеджер драйверов ODBC, драйвер, источник данных. Менеджер драйверов написан в виде DLL, которая загружается пользовательским приложением и перенаправляет вызовы функций ODBC API нужному драйверу. Драйвер, в свою очередь, выполняет основную работу по выполнению запросов.

Типичная схема взаимодействия приложения с базой данных состоит из трёх шагов:

  • установка соединения с БД
  • выполнение запросов на выборку и/или изменение данных в БД
  • разрыв соединения

ODBC API и классы MFC
MFC предоставляет набор классов, облегчающих работу с ODBC API. Два из них мы рассмотрим подробно - это CDatabase и CRecordset. Хотя эти два класса позволяют выполнять все основные операции по выборке и модификации данных, иногда их возможностей оказывается недостаточно. В этом случае приходится вызывать функции ODBC API напрямую (все эти функции имеют префикс SQL).

Источники данных
Источник данных (data source) - это по сути логическое имя базы данных, которое используется для обращения к ней средствами ODBC. Эта абстракция оказывается достаточно удобной: если база данных, используемая программой, будет скопирована в другой каталог или перенесена на другой компьютер, нужно просто скорректировать атрибуты источника данных, не внося никаких изменений в саму программу. Однако, ODBC позволяет работать с базой данных и напрямую, то есть без использования источников данных.

БАЗОВЫЕ ВОЗМОЖНОСТИ ODBC

Обработка ошибок
Прежде чем мы приступим к работе с ODBC, нужно научиться обрабатывать ошибки, которые могут возникнуть в процессе выполнения программы. Очень часто ответ на вопрос "почему программа не работает?" находится под рукой; нужно только знать, куда посмотреть.

При использовании ODBC API программист должен был анализировать возвращаемые значения функций, обращаясь за более подробной информацией об ошибке к функции SQLError. MFC скрывает от нас детали этого процесса. В случае возникновения ошибки она возбуждает исключение, которое и должна перехватить наша программа. Обработчик исключения получает указатель на структуру CDBException, которая содержит всю необходимую информацию. Так, поле m_strError содержит описание ошибки в понятной для человека форме, а в m_strStateNativeOrigin записывается пятибуквенный код состояния ODBC, который удобно анализировать в программе, а также некоторая дополнительная информация. Типичный пример кода обработки исключений выглядит так:

try
{
    // Работаем с БД
}

catch(CDBException *pException)
{
    AfxMessageBox(pException->m_strError);

    pException->Delete();    // Удаляем структуру из памяти!
}

Замечу, что обработку исключений следует использовать только для восстановления программы после ошибки. Если всё, что нам требуется - это просмотреть значения полей структуры CDBException, можно сделать это и без перехвата исключений. В режиме отладки Visual C++ сам выводит содержимое структуры CDBException на вкладку Debug в окно Output; так что если ваша программа "рухнула", следует первым делом посмотреть на эту вкладку.

Соединение с базой данных
Сначала поговорим о том, как в системе регистрируются источники данных. Это можно сделать программно или с помощью специальной утилиты - администратора источников данных ODBC. Эта утилита входит в состав Windows и вызывается через панель управления (пункт "Источники данных ODBC (32)"). Программную регистрацию мы рассмотрим во второй части, а пока можно воспользоваться услугами администратора.

Соединение с базой данных в MFC представляется объектом класса CDatabase. Чтобы установить соединение, необходимо воспользоваться функцией CDatabase::OpenEx (функция Open устарела; к тому же пользоваться ею менее удобно). Например:

CDatabase db;

db.OpenEx("DSN=db;UID=sa;PWD=", 0);

Первый параметр функции OpenEx - это строка подключения, в которой пары "параметр=значение" разделяются точкой с запятой. Имена параметров нечувствительны к регистру. В стандартный набор параметров строки соединения входят: DSN (data source name - имя источника данных), UID (имя пользователя), PWD (пароль) и DRIVER (драйвер ODBC). Большинство драйверов распознаёт ряд дополнительных параметров.

Обратите внимание, что строка соединения не должна начинаться с префикса "ODBC;" (этот префикс нужно было добавлять при использовании функции CDatabase::Open).

Второй параметр функции OpenEx - набор битовых флагов, объединённых логическим "ИЛИ". Вот некоторые из них:

CDatabase::openReadOnly - открыть БД в режиме "только для чтения".
CDatabase::noOdbcDialog - никогда не выводить диалог, запрашивающий дополнительную информацию о соединении.
CDatabase::forceOdbcDialog - всегда выводить диалог, запрашивающий информацию о соединении. Этот режим уместен, если во время написания программы не известно, с какой именно базой данных она будет работать.

По умолчанию (если второй параметр OpenEx не задан или равен 0) база данных открывается в режиме "чтение и запись", а диалог появляется только в случае, когда в строке соединения отсутствуют необходимые параметры (например, имя источника данных).

Выборка данных из таблицы
Работая с ODBC, программа получает данные, извлекаемые из БД, в виде множества записей (recordset). Каждая запись содержит набор полей. В любой заданный момент времени программа может работать только с одной записью (она называется текущей). Используя функции ODBC, можно перемещаться от одной записи к другой. Для удобства каждое поле в результирующем множестве обычно связывается с переменной, которую программа использует для чтения и модификации значения соответствующего поля. Тем не менее, связывать поля с переменными в общем случае необязательно.

В MFC для работы с множеством записей предназначен класс CRecordset. Как правило, этот класс не используется в программе напрямую. Вместо этого от него порождают новые классы, переменные-члены которых и связываются с полями множества записей. Само связывание происходит в виртуальной функции CRecordset::DoFieldExchange, которая переопределяется в производном классе; эта же функция осуществляет обмен данных между полями записи и переменными класса. Множество записей создаётся функцией CRecordset::Open, а перемещение от одной записи к другой осуществляется посредством функций Move, MoveNext, MovePrev и т. п.

Пример
Рассмотрим небольшой пример. Допустим, в базе данных содержится таблица tPeople с полями Name (типа строка из 50 символов) и DateOfBirth (типа дата/время), и мы хотим напечатать на экране её содержимое. Сперва создаём новый класс, порождённый от CRecordset:

class CPeople : public CRecordset
{
public:

    CPeople(CDatabase *pDatabase = NULL) :
        CRecordset(pDatabase) { m_nFields = 2; };

    CString m_Name;
    CTime m_DateOfBirth;

    void DoFieldExchange(CFieldExchange *pFX);
};

Для каждого поля в таблице tPeople мы объявили переменную соответствующего типа. Так, поле Name хранится как строка, поэтому m_Name имеет тип CString. DateOfBirth - это дата, поэтому m_DateOfBirth - переменная типа CTime. Обратите внимание, что в переменную m_nFields (CPeople наследует её от CRecordset) необходимо записать количество полей таблицы (это значение необходимо MFC, чтобы правильно построить запрос). Теперь реализуем функцию DoFieldExchange, в которой происходит связывание полей таблицы с переменными нашего класса CPeople.

void CPeople::DoFieldExchange(CFieldExchange *pFX)
{
    pFX->SetFieldType(CFieldExchange::outputColumn);

    RFX_Text(pFX, "Name", m_Name, 50);
    RFX_Date(pFX, "DateOfBirth", m_DateOfBirth);
}

Вызов SetFieldType с параметром CFieldExchange::outputColumn должен всегда предшествовать операциям связывания. Сам механизм связывания напоминает механизм обмена данными с элементами управления диалога. Тут и там используется набор специальных макросов, которые в зависимости от контекста (который определяется объектом класса CFieldExchange) выполняют различные действия - в нашем случае формируют элементы запроса, связывают переменные с полями множества записей и осуществляют обмен данными между ними.

Каждый из макросов, используемых в DoFieldExchange, имеет префикс "RFX_". Существует несколько версий этих макросов - по одному на каждый основной тип. Наборы параметров у них несколько отличаются, но первые три параметра совпадают у всех макросов: указатель на объект класса CFieldExchange (нужно просто передать указатель, полученный от MFC), имя поля во множестве записей и ссылка на переменную, которая будет с этим полем связана.

Итак, создание класса CPeople закончено, и можно использовать его для доступа к таблице tPeople. Вот фрагмент, который печатает на экране её содержимое.

:

CDatabase Db;

Db.OpenEx("DSN=db;UID=sa;PWD=");

CPeople Rs(&Db);

Rs.Open(CRecordset::dynaset, "tPeople");

while(!Rs.IsEOF())
{
    printf("%s\t%s\n",
           Rs.m_Name,
           Rs.m_DateOfBirth.Format("%d of %B %Y"));

    Rs.MoveNext();
}
:

Здесь нужно обратить внимание на несколько моментов. Во-первых, как мы помним, прежде чем работать с базой данных, необходимо установить соединение с ней. Это делается с использованием уже знакомой нам функцией CDatabase::OpenEx. Во-вторых, указатель на соединение нужно передать конструктору класса CPeople, чтобы данные извлекались именно из нужной нам базы данных.

Теперь рассмотрим параметры функции CRecordset::Open. Первый параметр задаёт тип результирующего множества записей. Можно задавать следующие типы:

CRecordset::forwardOnly - множество записей, доступное только для чтения и по которому можно перемещаться только вперёд.
CRecordset::snapshot - множество записей, по которому можно перемещаться в любом направлении. Изменения, внесённые в БД после создания такого множества, в нём не отражаются.
CRecordset::dynaset - похоже на предыдущее, но любые изменения записи в БД будут видны после повторной выборки этой записи. Новые записи, добавленные в БД после создания такого множества, в нём не отражаются.
CRecordset::dynamic - самое ресурсоёмкое множество. Любые изменения, внесённые в БД после его открытия, будут в нём отражены. Не поддерживается многими драйверами.

Если драйвер не поддерживает запрошенный тип множества записей, MFC возбудит исключение.

Второй параметр функции CRecordset::Open используется для передачи имени таблицы или запроса, на основе которого будет построено множество записей. MFC сама определит, что именно ей передали. У функции CRecordset::Open есть также третий параметр - который во многих случаях можно не указывать. За его описанием можно обратиться к документации.

После того как множество записей создано, пользоваться им достаточно просто. Для обращения к полям текущий записи мы используем переменные-члены класса CPeople, а к следующей записи перемещаемся при помощи функции CRecordset::MoveNext. Когда все записи исчерпаны, функция CRecordset::IsEOF возвращает TRUE, и цикл прерывается.

Модификация данных в таблице
С помощью методов класса CRecordset можно изменять записи в таблице и добавлять новые записи. Прежде чем изменять запись, следует убедиться, что открытое множество записей допускает такую операцию, с помощью функции CRecordset::CanUpdate. Сама модификация начинается вызовом функции CRecordset::Edit и завершается вызовом функции CRecordset::Update; между этими двумя вызовами следует изменить значения переменных, связанных с полями множества записей. Например:

if(Rs.CanUpdate())
{
   Rs.Edit();
   Rs.m_Name = "Vasya Pupkin";
   Rs.m_DateOfBirth = CTime(2000, 1, 1, 0, 0, 0);
   Rs.Update();
}

Аналогичным образом можно добавлять новые записи, но вместо Edit используется AddNew. Убедиться в том, что множество записей поддерживает добавление, можно с помощью функции CRecordset::CanAppend. Например:

if(Rs.CanAppend())
{
   Rs.AddNew();
   Rs.m_Name = "Vasya Pupkin";
   Rs.m_DateOfBirth = CTime(2000, 1, 1, 0, 0, 0);
   Rs.Update();
}

И последнее замечание. Чтобы обновить множество записей после внесения изменений в БД, нужно вызвать функцию CRecordset::Requery.

Разрыв соединения
Это самый простой, но совершенно необходимый этап. Закончив работу с источником данных, программа должна разорвать с ним соединение вызовом CDatabase::Close. Перед этим необходимо также закрыть все наборы записей, используя функцию CRecordset::Close. Ни одна из этих функций не принимает никаких параметров.

___

Если у вас есть какие-либо вопросы, предложения или пожелания, присылайте их мне по адресу rudankort@mail.ru. Я постараюсь учесть их при написании второй части статьи, которая будет посвящена более сложным аспектам работы с ODBC.

- Александр Шаргин

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

Q| Есть диалог на нем Date Time Picker и есть соответствующая ему переменная m_Time типа CTime. Проблема в том, что если m_Time = 0, то в диалоге высвечивается 2:00:00 !!?? Т.е. сдвиг на два часа. Причем если выставить 0:00:00, то будет "Assertion fault". Ну и соответственно, если установить 2:00:00, то после UpdateData() m_Time станет = 0. Скорее всего это как-то связано с часовым поясом (у меня часовой пояс +02:00). Как от этого избавиться? - Михаил

|A1 Как известно, класс CTime - это всего лишь объектная обёртка вокруг типа time_t из стандартной библиотеки языка C. А тип time_t (4 байта) хранит время как число секунд, прошедших с момента полуночи 1 января 1970 года. Это означает, что класс CTime не может хранить время ДО этого момента. А если записать в него 0, мы как раз и получим 1.01.1970, 0:00:00 (или 2:00:00 с учётом часового пояса).

Когда Date Time Picker работает в режиме ввода времени, он всё равно "помнит" полную дату. Если ввести в него "0:00:00", то с учётом часового пояса получится 31.12.1969, 22:00:00, то есть дата за пределами диапазона допустимых значений CTime. Это и приводит к срабатыванию ASSERT'а.

А для решения проблемы достаточно записать в CTime какую-нибудь дату, отличную от 1.01.1970. Например:

m_Time = CTime(2000, 1, 1, 0, 0, 0); // 1 января 2000 года

- Александр Шаргин

|A2 Суть "проблемы" в том, что элемент управления CDateTimeCtrl инкапсулирует одновременно и дату, и время , а не то или другое по отдельности. Как известно, тип данных time_t и класс CTime используют так называемый "UTC-based time" формат и хранят число секунд с ноля часов 1 января 1970 года (с учетом часового пояса). Поэтому при работе с CDateTimeCtrl это следует учитывать и использовать его именно в этом контексте (в "увязке" с датой). То есть, если мы инициируем переменную нулевым значением, то мы и имеем "точку отсчета".

Другой интересный вопрос состоит в том, как эффективно организовать корректировку даты и времени одновременно. К примеру: мы имеем переменную m_Time типа CTime с некоторым значением и хотим дать пользователю возможность изменить и время и дату, используя соответственно два элемента CDateTimeCtrl, чтобы в конечном итоге получить скорректированное значение m_Time. Вариант с созданием двух ассоциированных переменных CTime и последующим отбрасыванием у одной даты, у другой времени, и их сложением не очень красивый. Я решил это таким образом: создал ассоциированные переменные типа CDateTimeCtrl (а не CTime), использовал функцию SetTime(&m_Time) для установки даты и времени в обоих переменных, а потом при изменении любой из них считывал измененное значение функцией GetTime(&m_Time) и тут же корректировал значение "сопряженной" переменной функцией SetTime(&m_Time). Таким образом достаточно просто решилась проблема "синхронизации" изменения даты и времени.

- Евгений Шмелев

/ / / / ОБРАТНАЯ СВЯЗЬ / / / / / / / / /

Хотел бы дополнить Ваши материалы по .NET:
В частности, хотел бы указать на ошибки в No.20 от 22 октября 2000 г.
Платформа Microsoft.NET не базируется на сервисах COM+, а предлагает совершенно новое, более удобное множество сервисов. Так, вместо DCOM и COM+ Вам предоставляется Microsoft .NET Remoting (http://msdn.microsoft.com/library/default.asp?URL=/library/techart/hawkremoting.htm). Вместо каталога COM+ используется каталог .NET.
Естественно, компоненты .NET совместимы с компонентами COM+, в частности, и те, и другие прозрачно доступны друг другу через соответствующий уровень трансляции (flattened COM). Компиляция исходного кода возможна не только в IL, но и напрямую в машкод.
Первое преимущество .NET - настоящая объектность, включая наследование. Второе примущество - настоящая компонентная архитектура. Если кто-нибудь знаком с RAD-инструментарием Borland Delphi, то могу лишь сказать, что концепции .NET в области компонентной архитектуры, хранимых компонент и свойств, редакторов компонент и редакторов свойств являются органичным развитием идей, заложенных в Delphi VCL. Собственно, приглашаю на http://msdn.microsoft.com/net/default.asp

- Акжан Абдулин

[...] пару слов по поводу темы предыдущей рассылки. Я использую для решения этой проблемы опубликованный на нескольких сайтах класс CInstanceChecker ( http://www.naughter.com/sinstance.html ,автор PJ Naughter), который, по-моему, вполне успешно решает все описанные проблемы. В частности, там элегантно решен вопрос блокирования с помощью объекта класса CSingleLock повторно-запущенных копий вплоть до того момента, когда первая копия создаст главное окно для возможности его активизации.

И последнее. Порой не хочется заглядывать в русифицированные группы новостей из-за обилия "крутого профессионального жаргона" и некоторой агрессивности участников. Грустно за родной язык. Эдакое подражание новорусскому стилю: "пальцы веером" :). Кстати, англоязычные группы гораздо более традиционные и терпимые, хотя вопросы там бывают дилетантские, а профессиональный уровень отвечающих при этом очень высокий. В этом смысле эта рассылка приятно выделяется, и особо хочется отметить регулярные статьи Александра Шаргина.

- Евгений Шмелев

[...] хотелось бы также выразить благодарность Александру Шаргину, автору статьи на тему "отладчик" - несмотря на мной многолетний опыт узрел там несколько "приятных мелочей", проверить которые самому руки не доходили.
B свою очередь могу внести небольшое дополнение: если в окне отладчика Watch на одной из закладок поместить выраежение
<variable>=<value#1>
где variable - имя переменной, а value#1 - одно из возможных ее значений, то поместив на второй закладке Watch такую же строку с иным значением переменной, мы получаем очень удобный вариант быстрой установки/переключения значений интересующей нас переменной. чаще всего таковыми выступают логические переменные (хотя это и не обчзательно). если же у нас целый набор переменных, значения которых в процессе отладки нужно периодически менять, данный способ будет просто незаменим.

- Alexander Zasypkin

Благодарю всех, кто не поленился написать.

/ / / В ПОИСКАХ ИСТИНЫ / / / / / / / / / /

Q| 1. Есть окно нестандартной формы (например, круглое). Но рамка, появляющаяся вокруг него при перемещении, - строго прямоугольной формы. Как избавиться от такой рамки вообще? Или, может быть, ее можно сделать тоже произвольной формы (по контуру окна)?
2. Как избавиться от пунктирной рамки на кнопке, имеющей фокус? Для кнопки, сделанной из красивого рисунка, такая рамка выглядит лишней ... - Максим Чучуйко

Ответить на вопрос

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Это все на сегодня. Всего вам доброго!

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


No.28

Выпуск No. 28 от 17 декабря 2000 г.

Всем привет!

Сегодня вас ждет сюрприз. Сегодня в рассылке впервые появится статья по COM. Мне приходило очень много просьб по этому поводу, часто выражалось недовольство тем, что слишком много внимания уделяется MFC. Типичный пример - в рубрике "Обратная связь". Так что по зрелом размышлении я решил начать понемногу освещать в рассылке и тему COM. В связи с этим, тем кто никогда раньше с COM не сталкивался (а думаю среди подписчиков такие есть), предлагаю вам прочитать статью, знакомящую вас с этой технологией.

/ / / / СТАТЬЯ / / / / / / / / / / / / / / / / / / /

Введение в COM
 Часть 1

Автор: Michael Dunn
Перевод: Илья Простакишин
Источник: The Code Project

Предмет данной статьи

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

Введение

COM (Component Object Model - Объектно-Компонентная Модель) - одно из трехбуквенных сокращений, которые сегодня очень часто используются в Windows (вспомните - API, MFC, OLE, ATL). Множество новых технологий, разрабатывающихся постоянно, базируется на COM. Документация просто пестрит терминами типа COM object (объект COM), interface (интерфейс), server (сервер), и так далее, но везде почему-то предполагается, что вы уже знакомы с тем, как COM работает и как ее использовать.

Эта статья является вводной для начинающих, она описывает основные используемые механизмы, а также показывает как использовать объекты COM, поставляемые со стороны (особенно, оболочкой Windows). После знакомства со статьей вы сможете использовать COM-объекты, как встроенные в Windows, так и предоставляемые третьими лицами.

Я предполагаю, что вы уже являетесь специалистом в C++. Я частично использую в своих примерах MFC и ATL, и в этих случаях смысл кода будет поясняться, на случай, если вы не знакомы с этими библиотеками. [...]

Что такое COM?

COM - это метод разделения двоичного кода между разными приложениями, написанными на разных языках программирования. Это не совсем то, что обеспечивает С++, а именно повторное использование исходного кода. ATL - хороший пример такого подхода. Отлаженный исходный код может повторно использоваться и нормально работать только в C++. При этом существует возможность коллизий между именами, не говоря уже о неприятностях при наличии множества копий одинакового кода в ваших проектах.

Windows позволяет разделять код между приложениями с помощью библиотек DLL. Я не раскрою большого секрета, если скажу, что все функции Windows содержатся в различных внешних библиотеках - kernel32.dll, user32.dll и т.д., которые доступны любому Windows - приложению, и более того, должны им использоваться. Но DLL расчитаны на использование только посредством интерфейса С или языков, понимающих стандарты вызова языка C. Таким образом, реализация языка программирования является барьером между создаваемым приложением и уже реализованными процедурами, содержащимися внутри DLL-библиотеки.

В MFC был введен новый механизм разделения двоичного кода - библиотеки расширения MFC (MFC extension DLLs). Но это еще более ограниченный метод, т.к. вы можете использовать его только в приложениях, созданных на основе библиотеки MFC.

COM решает все эти проблемы. Делается это посредством введения двоичного стандарта. При этом спецификация COM требует, чтобы двоичные модули (DLL и EXE) компилировались в соответствие со специфической структурой, которая декларируется этим стандартом. Стандарт также в точности определяет, каким образом COM-объекты должны быть организованы в памяти. Вдобавок, двоичная структура не должна быть зависима от особенностей языка программирования (как, например, стандарта описаний имен в C++). Все это нужно для того, чтобы облегчить доступ к модулю приложения, созданного на любом языке программирования. Двоичный стандарт возлагает "бремя" совместимости на "плечи" компилятора, облегчая задачу вам, как создателю компонентов, и другим людям, которые будут пользоваться вашими компонентами.

Структура расположения COM-объектов в памяти очень похожа на модель, которая используется в C++ виртуальными функциями, поэтому многие компоненты COM создаются с использованием языка C++. Однако, здесь важно заметить, что язык, на котором вы пишите, не имеет значения, поскольку результат можно использовать в будущем с любыми языками программирования.

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

Основные определения

Начнем двигаться снизу-вверх. Итак, интерфейс (interface) - это простая группа функций. Эти функции, в свою очередь, называются методами (methods). Имена интерфейсов начинаются с буквы I, например IShellLink. В терминологии C++ интерфейс представляет собой абстрактный базовый класс, содержащий только чистые виртуальные функции (pure virtual functions).

Интерфейсы могут наследоваться (inherit) от других интерфейсов. Наследование работает также, как и одиночное наследование в C++. Множественное наследование для интерфейсов не применяется.

CO-класс (coclass) (сокращение от component object class) содержится в DLL или EXE и включает код одного или нескольких интерфейсов. Говорят, что CO-класс поддерживает или реализует (implement) эти интерфейсы. Объект COM (COM object) - это экземпляр CO-класса в памяти. Заметьте, что "класс" COM - это не тоже самое, что "класс" C++, хотя часто бывает, что класс COM реализуется посредством класса C++.

Сервер COM (COM server) - это двоичный файл (DLL или EXE), содержащий один или несколько CO-классов.

Регистрация (registration) - это процесс создания записей в реестре, которые сообщают Windows о том, где можно найти определенный сервер COM. Дерегистрация (unregistration) наоборот - удаление этих данных из реестра.

GUID (рифмуется с "fluid" - "жидкий, текучий", сокращение от globally unique identifier - Глобальный Уникальный Идентификатор) - это 128-битный номер, который используется COM для идентификации различных элементов. Каждый интерфейс и CO-класс имеет GUID. Коллизии между именами невозможны, поскольку каждый GUID абсолютно уникален и повторение GUID очень маловероятно (если вы используете для их создания функции COM API). Вы также можете иногда встретить термин UUID (сокращение от universally unique identifier). UUID и GUID это практически одно и тоже.

ID класса (class ID) или CLSID - это GUID, которым обозначается CO-класс. В свою очередь, ID интерфейса (interface ID), или IID - это GUID, обозначающий интерфейс.

Существует две причины, по которым идентификаторы GUID так широко используются в COM:

  1. GUID это всего лишь число. Любой язык программирования может оперировать им.

  2. Каждый GUID, создаваемый на любой машине, уникален (если создан правильно). Следовательно, два COM-разработчика не могут использовать одни и те же GUID. Это решает проблему по выделению уникальных GUID и устраняет необходимость в специальном центре по выделению GUID (как, например, при регистрации доменов в Internet).

HRESULT - это целочисленный тип, который используется COM для возврата кодов ошибок или кодов завершения. Не смотря на то, что имя типа начинается с префикса H, он (этот тип) не является дескриптором. Переменная типа HRESULT способна участвовать в любых логических операциях языка C, например != и ==.

Наконец, Библиотека COM (COM library) - это часть операционной системы, с которой вы взаимодействуете, когда делаете что-либо с элементами COM. Часто библиотека COM называется просто "COM" и иногда это приводит к некоторой путанице.

Работа с объектами COM

Каждый язык реализует операции с объектами по-разному. Например, в C++ вы создаете объекты на стеке, либо с помощью new динамически выделяете для них место в "куче". Поскольку COM должна быть нейтральна к языку, библиотека COM включает свои собственные средства управления объектами. Сравним управление объектами в COM и C++:

Создание нового объекта
  • В C++ используется оператор new, либо объект создается на стеке.

  • В COM вызывается специальная API-функция библиотеки COM.
Удаление объектов
  • В C++ используется оператор delete, либо объект удаляется автоматически при выходе из области видимости.

  • В COM каждый объект хранит свой собственный счетчик обращений. Когда вы заканчиваете работу с объектом, вы должны сообщить ему, что он вам больше не нужен. Когда счетчик обращений равен 0, объект сам выгружается из памяти.

Теперь, между этими двумя стадиями - создания и удаления объекта - вы, естественно, должны использовать этот объект. Когда вы создаете COM-объект, вы сообщаете библиотеке COM, какой интерфейс вам нужен. Если объект был успешно создан, библиотека COM возвращает указатель на запрашиваемый интерфейс. С его помощью вы можете вызывать методы этого интерфейса, также как при использовании обычного объекта C++.

Создание объекта COM

Для создания COM-объекта и получения интерфейса из этого объекта (напомню, что COM-объект может содержать несколько интерфейсов) вы должны вызвать библиотечную функцию CoCreateInstance(). Прототип CoCreateInstance():

HRESULT CoCreateInstance (
    REFCLSID  rclsid,
    LPUNKNOWN pUnkOuter,
    DWORD     dwClsContext,
    REFIID    riid,
    LPVOID*   ppv );

Описание параметров:

rclsid
CLSID CO-класса. Например, вы можете передать CLSID_ShellLink при создании COM-объекта, который используется для создания ярлыков.
pUnkOuter
Этот параметр используется только при агрегации COM-объектов, когда берется существующий CO-класс и в него добавляются новые методы. Для наших целей мы должны передать NULL для указания на то, что агрегация использоваться не будет.
dwClsContext
Указывает на тип COM-сервера. В этой статье будет использоваться простейший тип сервера - in-process DLL, поэтому в качестве параметра будет передаваться константа CLSCTX_INPROC_SERVER. Предостережение: не используйте CLSCTX_ALL (она установлена в ATL по умолчанию), т.к. это может привести к ошибке в системах Windows 95, где не инсталлирован DCOM.
riid
Это IID интерфейса, который вы хотите получить. Например, вы должны передать IID_IShellLink для получения указателя на интерфейс IShellLink.
ppv
Адрес указателя на интерфейс. Библиотека COM возвращает указатель на запрашиваемый интерфейс через этот параметр.

Когда вы вызываете CoCreateInstance(), она находит CLSID в реестре, считывает данные о расположении сервера, загружает сервер в память и создает экземпляр CO-класса, который вы запрашивали.

Вот пример, в котором создается объект CLSID_ShellLink и запрашивается указатель на интерфейс IShellLink , которым владеет этот COM-объект.

HRESULT     hr;
IShellLink* pISL;

    hr = CoCreateInstance ( CLSID_ShellLink,  // CLSID CO-класса
                 NULL,    // агрегация не используется
                 CLSCTX_INPROC_SERVER,  // тип сервера
                 IID_IShellLink,   // IID интерфейса
                 (void**) &pISL );  // Указатель на наш интерфейсный указатель

    if ( SUCCEEDED ( hr ) )
        {
        // Здесь можно вызывать методы, используя pISL.
        }
    else
        {
        // Невозможно создать объект COM. hr присвоен код ошибки.
        }

В начале мы объявляем переменную типа HRESULT для хранения значения, возвращаемого CoCreateInstance() и указатель на IShellLink. Затем мы вызываем CoCreateInstance() для создания нового COM-объекта. Макрос SUCCEEDED возвращает TRUE, если hr хранит код успешного завершения, или FALSE, если hr содержит код ошибки. Есть также похожий макрос - FAILED, который проверяет значение на предмет соответствия коду ошибки (т.е. делает все наоборот).

Удаление COM-объекта

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

Продолжим предыдущий пример, добавив команду удаления объекта:

// Создаем COM-объект как раньше и...

    if ( SUCCEEDED ( hr ) )
        {
        // Вызов методов интерфейса через pISL.

        // Сообщим COM-объекту о том, что он нам больше не нужен.
        pISL->Release();
        }

Интерфейс IUnknown будет детально рассмотрен в следующем разделе.

[Продолжение следует]

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

Q| 1. Есть окно нестандартной формы (например, круглое). Но рамка, появляющаяся вокруг него при перемещении, - строго прямоугольной формы. Как избавиться от такой рамки вообще? Или, может быть, ее можно сделать тоже произвольной формы (по контуру окна)?
2. Как избавиться от пунктирной рамки на кнопке, имеющей фокус? Для кнопки, сделанной из красивого рисунка, такая рамка выглядит лишней ... - Максим Чучуйко

|A 1. Избавиться от рамки можно так. Как известно, в Windows существует настройка, определяющая двигаются ли окна целиком или двигается только рамка, а окно переносится на новое место после отпускания кнопки мыши. Менять эту настройку можно либо через панель управления, либо программно - с помощью функции SystemParametersInfo. Таким образом, нужно включить режим перетаскивания окна целиком, когда наше окно начинают перемещать, и вернуть его в первоначальное положение после того, как перемещение закончено.

О том, что перемещение начинается, окно узнаёт по сообщению WM_SYSCOMMAND (с параметром SC_MOVE). Когда перемещение завершается, окно получает ещё одно сообщение - WM_EXITSIZEMOVE. Обработчики могут выглядеть так:

void CMainFrame::OnSysCommand(UINT nID, LPARAM lParam)
{
    if((nID & 0xFFF0) == SC_MOVE)
    {
        SystemParametersInfo(SPI_GETDRAGFULLWINDOWS, 0, &m_bDrag, 0);
        SystemParametersInfo(SPI_SETDRAGFULLWINDOWS, TRUE, 0, 0);
    }

    CFrameWnd::OnSysCommand(nID, lParam);
}

LRESULT CMainFrame::OnExitSizeMove(WPARAM wParam, LPARAM lParam)
{
    if(m_bDrag != -1)
    {
        SystemParametersInfo(SPI_SETDRAGFULLWINDOWS, m_bDrag, 0, 0);
        m_bDrag = -1;
    }

    return Default();
}

Переменную m_bDrag типа int следует добавить с класс главного окна и инициализировать значением -1 в конструкторе.

Обратите внимание, что ClassWizard не умеет вставлять обработчик WM_EXITSIZEMOVE - придётся сделать это вручную, используя макрос ON_MESSAGE.

2. Сперва порекомендую метод, который широко применяют парни из Microsoft - использовать вместо кнопки Tool bar с одной-единственной кнопкой. Нужно только установить такому тулбару стили CCS_NOPARENTALIGN и CCS_NORESIZE, чтобы он не прижимался к верхней кромке окна, а оставался там, где мы его разместили. Этот же способ, кстати, можно использовать, если в приложении требуется "нормальная" плоская кнопка.

Ну а если такой способ не подходит, остаётся прибегнуть к custom draw. Это не должно быть проблемой, так как изображение для кнопки уже нарисовано - осталось добавить к нему выпуклую/вдавленную кромку.

- Александр Шаргин

/ / / / ОБРАТНАЯ СВЯЗЬ / / / / / / / / / / / / / / /

Я уже давно получаю вашу подписку. Она мне очень нравится. Но у меня всё время возникает вопрос когда я читаю очередной номер подписки. Почему почти все выпуски так или иначе посвещены MFC? Даже если тема к примеру ODBC, то примеры всё равно на MFC? Я не имею ничего против MFC, но сам последний раз писал на ней уже очень давно потому-что MFC болше всё-же desktop-UI-ориентированная. То чем я занимаюсь и надеюсь не только я J. Написанием COM, COM+ компонентов с UI обычно на ASP. Компоненты я пишу на ATL с STL, с доступом к базам данных через OLE DB/ADO. По ATL/STL/COM/COM+/OLE DB/ADO довольно мало материала в подписке. Почему? Неужели подавляющее большинство подписчиков пишет только на MFC?

- Vladislav Loidap

/ / / В ПОИСКАХ ИСТИНЫ / / / / / / / / / / / / / /

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

Ответить на вопрос

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

А на сегодня это все... До скорого!

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


No.29

Выпуск No. 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 системами.
У Уважением

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

На вопрос из выпуска No.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.


No.30

Выпуск No. 30 от 28 января 2001 г.

Здравствуйте, дорогие друзья!

Я очень рад вас всех вновь приветствовать! К сожалению, по не зависящим от меня причинам я не имел возможности выпускать рассылку вплоть до настоящего времени. Искренне прошу извинить за причиненные неудобства и такой вот вынужденный перерыв. Хочу развеять опасения некоторых товарищей: рассылку я закрывать не собираюсь. Начиная с сегодняшнего дня, выпуски будут опять выходить регулярно.

За это время количество подписчиков перевалило за 10 000 - действительно круглое число! Создавая рассылку, я и не предполагал, что она будет пользоваться такой популярностью - все-таки весьма специфичная тематика. Но это значит, что рассылка актуальна, и это не может не радовать. Что еще могу сказать - оставайтесь с нами, и скорее всего не пожалеете! А теперь - let's get started!

/ / / / СТАТЬЯ / / / / / / / / / / / / / / / / / / / / / /

Помнится, в одном из декабрьских выпусков шел у нас разговор о предотвращении запуска второй копии приложения. Тогда мы затронули тему использования объектов синхронизации, подробнее про которые я пообещал рассказать во второй части статьи про многозадачность. Тема эта хотя и очень интересная, но и довольно обширная; так что учитывая ограниченность места в одном выпуске, я освещу только самые важные для понимания моменты. Некоторые же второстепенные темы - такие, как предотвращение взаимного блокирования потоков, или оповещения об изменениях, - я здесь лишь упомяну, и (возможно) вынесу в дальнейшем в отдельную статью. Также в отдельную статью скорее всего выльется очень важная тема межпроцессного обмена данными (inter-process communication, IPC). Как скоро появятся эти статьи, будет зависеть от степени их востребованности. А пока представляю вашему вниманию давно обещанную вторую часть статьи про многозадачность.

Многозадачность и ее применение
 Часть 2: Синхронизация потоков

Итак, в первой части статьи (см. No.23) мы определили, что использование многопоточности находит себе широчайшее применение в самых различных программах и позволяет значительно повысить производительность и надежность приложений и системы в целом, сделать работу пользователя более комфортной, а также несколько упростить логику программы, производя естественное разделение обязанностей между потоками. Настоящий программист под Windows должен знать и уметь использовать преимущества операционной системы, одним из которых как раз и является вытесняющая многозадачность.

Необходимость синхронизации
Если вы помните, в Windows выполняются не процессы, а потоки. При создании процесса автоматически создается его основной поток. Этот поток в процессе выполнения может создавать новые потоки, которые, в свою очередь, тоже могут создавать потоки и т.д. Процессорное время распределяется именно между потоками, и получается, что каждый поток работает независимо.

Все потоки, принадлежащие одному процессу, разделяют некоторые общие ресурсы - такие, как адресное пространство оперативной памяти или открытые файлы. Эти ресурсы принадлежат всему процессу, а значит, и каждому его потоку. Следовательно, каждый поток может работать с этими ресурсами без каких-либо ограничений. Но так ли это в действительности? Вспомним, что в Windows реализована вытесняющая многозадачность - это значит, что в любой момент система может прервать выполнение одного потока и передать управление другому. (Раньше использовался способ организации, называемый кооперативной многозадачностью. Система ждала, пока поток сам не соизволит передать ей управление. Именно поэтому в случае глухого зависания одного приложения приходилось перезагружать компьютер. Так была организована, например, Windows 3.1). Что произойдет, если один поток еще не закончил работать с каким-либо общим ресурсом, а система переключилась на другой поток, использующий тот же ресурс? Произойдет штука очень неприятная, я вам это могу с уверенностью сказать, и результат работы этих потоков может чрезвычайно сильно отличаться от задуманного. Такие конфликты могут возникнуть и между потоками, принадлежащими различным процессам. Всегда, когда два или более потоков используют какой-либо общий ресурс, возникает эта проблема.

Именно поэтому необходим механизм, позволяющий потокам согласовывать свою работу с общими ресурсами. Этот механизм получил название механизма синхронизации потоков (thread synchronization).

Структура механизма синхронизации
Что же представляет собой этот механизм? Это набор объектов операционной системы, которые создаются и управляются программно, являются общими для всех потоков в системе (некоторые - для потоков, принадлежащих одному процессу) и используются для координирования доступа к ресурсам. В качестве ресурсов может выступать все, что может быть общим для двух и более потоков - файл на диске, порт, запись в базе данных, объект GDI, и даже глобальная переменная программы (которая может быть доступна из потоков, принадлежащих одному процессу).

Объектов синхронизации существует несколько, самые важные из них - это взаимоисключение (mutex), критическая секция (critical section), событие (event) и семафор (semaphore). Каждый из этих объектов реализует свой способ синхронизации. Какой из них следует использовать в каждом конкретном случае вы поймете, подробно познакомившись с каждым из этих объектов. Также в качестве объектов синхронизации могут использоваться сами процессы и потоки (когда один поток ждет завершения другого потока или процесса); а также файлы, коммуникационные устройства, консольный ввод и уведомления об изменении (к сожалению, освещение этих объектов синхронизации выходит за рамки данной статьи).

В чем смысл объектов синхронизации? Каждый из них может находиться в так называемом сигнальном состоянии. Для каждого типа объектов это состояние имеет различный смысл. Потоки могут проверять текущее состояние объекта и/или ждать изменения этого состояния и таким образом согласовывать свои действия. Что еще очень важно - гарантируется, что когда поток работает с объектами синхронизации (создает их, изменяет состояние) система не прервет его выполнения, пока он не завершит это действие. Таким образом, все конечные операции с объектами синхронизации являются атомарными (неделимыми), как бы выполняющимися за один такт.

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

Работа с объектами синхронизации
Чтобы создать тот или иной объект синхронизации, производится вызов специальной функции WinAPI типа Create... (напр. CreateMutex). Этот вызов возвращает дескриптор объекта (HANDLE), который может использоваться всеми потоками, принадлежащими данному процессу. Есть возможность получить доступ к объекту синхронизации из другого процесса - либо унаследовав дескриптор этого объекта, либо, что предпочтительнее, воспользовавшись вызовом функции открытия объекта (Open...). После этого вызова процесс получит дескриптор, который в дальнейшем можно использовать для работы с объектом. Объекту, если только он не предназначен для использования внутри одного процесса, обязательно присваивается имя. Имена всех объектов должны быть различны (даже если они разного типа). Нельзя, например, создать событие и семафор с одним и тем же именем.

По имеющемуся дескриптору объекта можно определить его текущее состояние. Это делается с помощью т.н. ожидающих функций. Чаще всего используется функция WaitForSingleObject. Эта функция принимает два параметра, первый из которых - дескриптор объекта, второй - время ожидания в мсек. Функция возвращает WAIT_OBJECT_0, если объект находится в сигнальном состоянии, WAIT_TIMEOUT - если истекло время ожидания, и WAIT_ABANDONED, если объект-взаимоисключение не был освобожден до того, как владеющий им поток завершился. Если время ожидания указано равным нулю, функция возвращает результат немедленно, в противном случае она ждет в течение указанного промежутка времени. В случае, если состояние объекта станет сигнальным до истечения этого времени, функция вернет WAIT_OBJECT_0, в противном случае функция вернет WAIT_TIMEOUT.
Если в качестве времени указана символическая константа INFINITE, то функция будет ждать неограниченно долго, пока состояние объекта не станет сигнальным.
Если необходимо узнавать о состоянии сразу нескольких объектов, следует воспользоваться функцией WaitForMultipleObjects.
Чтобы закончить работу с объектом и освободить дескриптор вызывается функция CloseHandle.

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

Теперь давайте рассмотрим каждый тип объектов синхронизации в отдельности.

Взаимоисключения
Объекты-взаимоисключения (мьютексы, mutex - от MUTual EXclusion) позволяют координировать взаимное исключение доступа к разделяемому ресурсу. Сигнальное состояние объекта (т.е. состояние "установлен") соответствует моменту времени, когда объект не принадлежит ни одному потоку и его можно "захватить". И наоборот, состояние "сброшен" (не сигнальное) соответствует моменту, когда какой-либо поток уже владеет этим объектом. Доступ к объекту разрешается, когда поток, владеющий объектом, освободит его.

Для того, чтобы объявить взаимоисключение принадлежащим текущему потоку, надо вызвать одну из ожидающих функций. Поток, которому принадлежит объект, может его "захватывать" повторно сколько угодно раз (это не приведет к самоблокировке), но столько же раз он должен будет его освобождать с помощью функции ReleaseMutex.

События
Объекты-события используются для уведомления ожидающих потоков о наступлении какого-либо события. Различают два вида событий - с ручным и автоматическим сбросом. Ручной сброс осуществляется функцией ResetEvent. События с ручным сбросом используются для уведомления сразу нескольких потоков. При использовании события с автосбросом уведомление получит и продолжит свое выполнение только один ожидающий поток, остальные будут ожидать дальше.

Функция CreateEvent создает объект-событие, SetEvent - устанавливает событие в сигнальное состояние, ResetEvent-сбрасывает событие. Функция PulseEvent устанавливает событие, а после возобновления ожидающих это событие потоков (всех при ручном сбросе и только одного при автоматическом), сбрасывает его. Если ожидающих потоков нет, PulseEvent просто сбрасывает событие.

Семафоры
Объект-семафор - это фактически объект-взаимоисключение со счетчиком. Данный объект позволяет "захватить" себя определенному количеству потоков. После этого "захват" будет невозможен, пока один из ранее "захвативших" семафор потоков не освободит его. Семафоры применяются для ограничения количества потоков, одновременно работающих с ресурсом. Объекту при инициализации передается максимальное число потоков, после каждого "захвата" счетчик семафора уменьшается. Сигнальному состоянию соответствует значение счетчика больше нуля. Когда счетчик равен нулю, семафор считается не установленным (сброшенным).

Критические секции
Объект-критическая секция помогает программисту выделить участок кода, где поток получает доступ к разделяемому ресурсу, и предотвратить одновременное использование ресурса. Перед использованием ресурса поток входит в критическую секцию (вызывает функцию EnterCriticalSection). Если после этого какой-либо другой поток попытается войти в ту же самую критическую секцию, его выполнение приостановится, пока первый поток не покинет секцию с помощью вызова LeaveCriticalSection. Похоже на взаимоисключение, но используется только для потоков одного процесса.

Существует также функция TryEnterCriticalSection, которая проверяет, занята ли критическая секция в данный момент. С ее помощью поток в процессе ожидания доступа к ресурсу может не блокироваться, а выполнять какие-то полезные действия.

Защищенный доступ к переменным
Существует ряд функций, позволяющих работать с глобальными переменными из всех потоков не заботясь о синхронизации, т.к. эти функции сами за ней следят. Это функции InterlockedIncrement/InterlockedDecrement, InterlockedExchange,InterlockedExchangeAdd и InterlockedCompareExchange. Например, функция InterlockedIncrement увеличивает значение 32-битной переменной на единицу - удобно использовать для различных счетчиков. Более подробно об этих функциях см. в документации.

Пример
Как известно, теория лучше всего познается на примерах. Давайте рассмотрим небольшой пример работы с объектом-взаимоисключением. Для простоты я использовал консольное Win32 приложение, но как вы понимаете, это совершенно не обязательно.

#include <windows.h>
#include <iostream.h>

void main()
{
    DWORD res;

    // создаем объект-взаимоисключение
    HANDLE mutex = CreateMutex(NULL, FALSE, "APPNAME-MTX01");
    // если он уже существует, CreateMutex вернет дескриптор существующего объекта,
    // а GetLastError вернет ERROR_ALREADY_EXISTS

    // в течение 20 секунд пытаемся захватить объект
    cout<<"Trying to get mutex...\n"; cout.flush();
    res = WaitForSingleObject(mutex,20000);

    if (res == WAIT_OBJECT_0) // если захват удался
    {
          // ждем 10 секунд
          cout<<"Got it! Waiting for 10 secs...\n"; cout.flush();
          Sleep(10000);

          // освобождаем объект
          cout<<"Now releasing the object.\n"; cout.flush();
          ReleaseMutex(mutex);
    }

    // закрываем дескриптор
    CloseHandle(mutex);
}

Для проверки работы мьютекса запустите сразу два экземпляра этого приложения. Первый экземпляр сразу захватит объект и освободит его только через 10 секунд. Только после этого второму экземпляру удастся захватить объект. В данном примере объект используется для синхронизации между процессами, поэтому он обятельно должен иметь имя.

Cинхронизация в MFC
Библиотека MFC содержит специальные классы для синхронизации потоков (CMutex, CEvent, CCriticalSection и CSemaphore). Эти классы соответствуют объектам синхронизации WinAPI и являются производными от класса CSyncObject. Чтобы понять, как их использовать, достаточно просто взглянуть на конструкторы и методы этих классов - Lock и Unlock. Фактически эти классы - всего лишь обертки для объектов синхронизации.

Eсть еще один способ использования этих классов - написание так называемых потоково-безопасных классов (thread-safe classes). Потоково-безопасный класс - это класс, представляющий какой либо ресурс в вашей программе. Вся работа с ресурсом осуществляется только через этот класс, который содержит все необходимые для этого методы. Причем класс спроектирован таким образом, что его методы сами заботятся о синхронизации, так что в приложении он используется как обычный класс. Объект синхронизации MFC добавляется в этот класс в качестве закрытого члена класса, и все функции этого класса, осуществляющие доступ к ресурсу, согласуют с ним свою работу.

С классами синхронизации MFC можно работать как напрямую, используя методы Lock и Unlock, так и через промежуточные классы CSingleLock и CMultiLock (хотя на мой взгляд, работать через промежуточные классы несколько неудобно. Но использование класса СMultiLock необходимо, если вы хотите следить за состоянием сразу нескольких объектов).

Заключение
Игнорируя возможности многозадачности, которые предоставляет Windows, вы игнорируете преимущества этой операционной системы. Как вы могли убедиться, многозадачность - это вовсе не так сложно, как кажется на первый взгляд.

Тем, кто интересуется этой темой, могу порекомендовать следующие статьи и разделы MSDN:

  • Platform SDK / Windows Base Services / Executables / Processes and Threads
  • Platform SDK / Windows Base Services / Interprocess Communication / Synchronization
  • Periodicals 1996 / MSJ / December / First Aid For Thread-impaired:Using Multiple Threads with MFC
  • Periodicals 1996 / MSJ / March / Win32 Q&A
  • Periodicals 1997 / MSJ / July / C++ Q&A.
  • Periodicals 1997 / MSJ / January / Win32 Q&A.

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

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

    |A1 Большинство средств дистрибутирования (InstallShield, Wise, Windows Installer, etc.) позволяют регистрировать ActiveX-элементы в процессе инсталляции. При инсталляции "руками" можно вызвать regsvr32.exe и передать ей параметром исполняемый файл ActiveX-элемента. Если Вы сами пишете программу инсталляции - вызовите ф-ию DllRegisterServer из исполняемого файла ActiveX. Но при этом помните - для использования чужого ActiveX в коммерческих проектах необходимо иметь на то лицензию. - Andrew Shvydky
    |A2 Сначала необходимо учесть, что перед запуском программы на другом компьютере, в случае добавления в свой проект ActiveX (COM) компонентов,их необходимо будет перенести и зарегестрировать в реестре.
    Ответ несколькими способами:
    1. Первый И пожалуй самый надежный это сделать инсталяшку, которая будет заниматься, помимо установки, регистрацией ActiveX компонентов. (ActiveX надо включить в инсталяшку)
    2. Это написать .bat файл, в который включить строки regsvr32.exe my.ocx ... и принести на другой комп свой .exe, .ocx,и этот .bat файл, перед первым запуском запустить .bat который зарегистрирует твой ActiveX в системе, а далее запускай программу. (Стандартная программа Window regsvr32.exe, займется регистрацией ActiveX компонента в системе)
    3. Это самый утомительный, на другом компьютере через командную строку использую программу regsvr32.exe вручную зарегестрировать свои ActiveX компоненты. - Оleg Zhuk

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

    / / / / ОБРАТНАЯ СВЯЗЬ / / / / / / / / / / / / / /

    Хочу рассказать о решении одной проблемы, с которой я сам много провозился, да и многие другие тоже... Речь идет об инсталляции MSDN на компьютере, где уже установлен MSOffice 2000. Проблема возникает при регистрации коллекции справочников. Решение следующее: перенести файл C:\WINDOWS\HELP\HHCOLREG.DAT на другое место, а после установки MSDN объединить его с новым файлом на том же месте. Файл имеет простую текстовую структуру (XML) и разобраться в нем не составит труда. Другой вариант решения - ставить сначала MSDN, а уже затем Office. - Никита Зимин
    Прочитав дополнение Алексея Трошина к статье о DCOM по поводу реализации DCOM на платформах, отличных от Windows, решил внести и свою небольшую лепту. Дело в том, что существует, и уже довольно давно (в течении нескольких лет) реализация DCOM для нескольких платформ, включая различные варианты UNIX систем, IBM mainfraim и OpenVMS. Семейство продуктов носит название EntireX и реализовано это немецкой компанией Software AG. Более подробная информация есть на их сайте: http://www.softwareag.com/entirex/technical/data.htm
    Более того, эта же компания предоставляет бесплатную версию данного продукта для Linux, ее можно скачать отсюда: http://www.softwareag.com/entirex/download/free_download.htm Пакет включает в себя реализацию многих компонентов DCOM, вкючая подмножество Win32 API, Structured Storage, Automation, ATL версии 2.1 и др.
    Самое интересное, что все это даже работает :-) У нас был опыт успешного портирования Win32 DCOM сервиса, основанного на ATL под Linux платформу с использованием данного продукта.
    Одним из существенных недостатков данного продукта является цена версий для не-Linux платформ - нам ее, например, так ни разу и не назвали, наверное чтобы не отпугивать сразу :-), поскольку полагаю, она не меленькая.
    Прошу ни в коем случае не принимать мое письмо как рекламу данного продукта :-))) Я не имею никакого отношения к компании Software AG, просто подумал, что вам будет интересно об этом всем узнать. - Антон Масловский

    / / / В ПОИСКАХ ИСТИНЫ / / / / / / / / / / / / /

    Q| У меня одна проблема: Пишу одну программку (написал уже довольно много) используя Win32API. И у меня возникла проблема со ScrollBar'ами. Вся загвоздка в том, что позиция бегунка прокрутки описана как short int и соответственно лежит в двухбайтном диапазоне. А в моей программе диапазон прокрутки может быть больше чем 32767. В хелпе на сообщение WM_VSCROLL советуют использовать функцию GetScrollPos, у меня че-то не получилось ее использовать. Как решить эту проблему? - Алексей Иванов

    Ответить на вопрос

    / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

    Ну вот, на сегодня хватит. И так выпуски получаются довольно объемными. Кстати, хочу всем сказать: я НЕ высылаю архив выпусков по почте. Если вы хотите посмотреть старые выпуски, добро пожаловать в архив на Subscribe.ru.
    До встречи!

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


No.31

Выпуск No. 31 от 4 февраля 2001 г.

Всем привет!

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

/ / / / СТАТЬЯ / / / / / / / / / / / / / / / / / / / / / /

Пространство имен оболочки Windows

Автор: Акжан Абдулин
Cтатья публикуется с сокращениями.
Полную версию этой статьи (с примерами),
а также много другой полезной информации,
вы можете найти на сайте автора по адресу
http://www.akzhan.midi.ru.

В операционных системах компании Microsoft с 1995 года используется новая оболочка, построенная на основе компонентной объектной модели. Одним из нововведений оболочки операционной системы стало понятие пространства имён оболочки. Пространство имён оболочки являет собой иерархически упорядоченный мир объектов, известных операционной системе, с их свойствами и предоставляемыми действиями. Оно во многом сходно со структурой файловой системы, но включает в себя не только файлы и каталоги. Такие понятия файловой системы, как имя файла и путь, заменены более универсальными.

Основное пространство имён начинается с корневого объекта "Рабочий стол", и его легко исследовать, запустив приложение "Проводник". Параллельно основному пространству имён могут сосуществовать множество дополнительных пространств имён, о которых подробнее будет рассказано позднее.

Основные понятия

Пространство имён (Shell namespace) является древовидной структурой, состоящей из COM-объектов. Объекты, владеющие дочерними объектами, именуются папками (Shell folder), причём среди таковых могут оказаться и другие папки (Subfolders). Объекты, не владеющие дочерними объектами, именуются файловыми объектами (file objects), причём файловым объектом может представлять собой не только файл файловой системы, но и принтер, компонент "Панели Управления" или объект другого типа. Каждый объект имеет идентификатор элемента (Item identifier), однозначно определяющий его расположение в папке. Таким образом, чтобы указать на некий объект в данной папке, нам потребуется лишь передать его идентификатор. Если же мы хотим указать на некий объект в известном пространстве имён, тогда нам придётся указать идентификаторы всех папок, начиная с корня, и до самого объекта включительно. В качестве примера приведём аналогию из файловой системы: "C:\Мои документы\Доклад о возможных способах реализации интерфейса к корпоративной БД.doc" уникально представит файл относительно файловой системы известного (моего домашнего) компьютера.

То, что в файловой системе именуется путём к файлу, в пространстве имён именуется списком идентификаторов (Identifier List).

Объекты-папки знают о тех обьектах, которыми они владеют, и о тех операциях, которые с ними возможны. Папки предоставляют нам механизм для перечисления всех объектов, которыми данный объект-папка владеет - интерфейс IShellFolder. Получение от объекта указателя на данный интерфейс называется привязкой (Binding).

Большая часть объектов основного пространства имён оболочки являются объектами, представляющими часть файловой системы. Те же объекты, что не представлены в файловой системе, называются виртуальными. Такие виртуальные папки, как папки рабочего стола (Desktop), "Мой Компьютер" (My Computer) и "Сетевое окружение" (Network Neighborhood), позволяют реализовать унифицированное пространство имён.

Каталоги файловой системы, используемые оболочкой в особых целях, называются специальными. Одной из таких папок, например, является папка "Программы" (Programs). Местонахождение специальных папок файловой системы указывается в подразделе ветви HKEY_CURRENT_USER/Software/Microsoft/Windows/CurrentVersion/Explorer/Shell Folders/.

Идентификаторы элементов

Идентификатор элемента является уникальным для той папки, в которой данный элемент (объект пространства имён оболочки) находится, и является двоичной структурой переменного размера, чей формат определяется тем программным обеспечением, которое поддерживает существование папки, владеющей определяемым данным идентификатором объектом. Идентификатор элемента имеет смысл только в контексте той папки, которая его сконструировала.

Идентификатор элемента описывается структурой SHITEMID, для которой определено лишь значение первого поля - размер данной структуры.

Список идентификаторов, уникально идентифицирующих объект в определённом пространстве имён, эквивалентен понятию пути для файловой системы, и определяется как список из последовательно расположенных идентификаторов, за которыми следует завершающее список 16-битное значение 0x0000 (ITEMIDLIST). Список идентификаторов может быть как абсолютным, то есть определяющим положение объекта относительно корневой папки, так и относительным, то есть определяющим положение элемента относительно какой-либо конкретной папки.

Приложение оперирует понятием указателя на список идентификаторов (Pointer to an Identifier List), который кратко именуют как PIDL-указатель. Все глобальные методы (утилиты) оболочки, принимающие в качестве одного из параметров PIDL-указатель, ожидают его в абсолютном формате. В то же время все методы интерфейса IShellFolder, принимающие в качестве одного из параметров PIDL-указатель, ожидают его в относительном формате (если только в описании метода не указано иначе).

Ниже представлена функция, позволяющая получить указатель на следующий элемент в списке идентификаторов. В случае неудачи возвращается пустой указатель.

#include <shlobj.h>

LPITEMIDLIST GetNextItemID(const LPITEMIDLIST pidl)
{
  size_t cb = pidl->mkid.cb;
  if( cb == 0 )
  {
    return NULL;
  }
  pidl = (LPITEMIDLIST) (((LPBYTE) pidl) + cb);
  if( pidl->mkid.cb == 0 )
  {
    return NULL;
  }
  return pidl;
}

За размещение списков идентификаторов отвечает распределитель памяти оболочки (Shell's allocator), предоставляющий интерфейс IMalloc. Указатель на данный интерфейс распределителя памяти оболочки можно получить через метод SHGetMalloc.

Таким образом, если Ваше приложение получает от оболочки PIDL-указатель, то оно становится ответственным за обязательное в дальнейшем освобождение этого списка с помощью распределителя памяти оболочки.

Ниже представлен пример копирования списка идентификаторов:

#include <shlobj.h>

size_t GetItemIDListSize(const LPITEMIDLIST pidl)
{
  size_t size = 0;
  LPBYTE p = LPBYTE(pidl);
  while( p != NULL )
  {
    if( static_cast(p + size)->mkid.cb == 0 )
    {
      size += sizeof( USHORT ); // size of terminator;
      break;
    }
    size += static_cast(p + size)->mkid.cb;
  }
  return size;
}

LPITEMIDLIST CopyItemIDList(const LPITEMIDLIST pidl)
{
  LPMALLOC pMalloc;
  LPITEMIDLIST pidlResult;
  if( pidl == NULL )
  {
    return NULL;
  }
  if( ! SUCCEEDED(SHGetMalloc(& pMalloc))
  {
    return NULL;
  }
  size_t size = GetItemIDListSize(pidl);
  pidlResult = pMalloc->Alloc( size );
  if( pidlResult != NULL )
  {
    CopyMemory( pidlResult, pidl, size );
  }
  pMalloc->Release();
  return pidlResult;
}

Для увеличения эффективности работы Ваших приложений рекомендуется брать ссылку на распределитель памяти оболочки при запуске приложения, и освобождать эту ссылку при выходе из приложения.

Интерфейс IShellFolder предоставляет метод CompareIDs для определения расположения двух идентификаторов относительно друг друга (выше, ниже или равны) в данной папке. При этом параметр lParam определяет критерий упорядочивания, но заранее определённым для всех объектов-папок является только сортировка по имени (значение 0). Если вызов этого метода завершён успешно, то поле CODE возвращаемого значения содержит ноль при равенстве объектов, отрицательно, если первое меньше второго, и положительно в обратном случае.

  hr = ppsf->CompareIDs(0, pidlA, pidlB);
  if( SUCCEEDED(hr) )
  {
    iComparisonResult = short(HRESULT_CODE(hr))
  }

Местонахождение объектов-папок

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

SHGetDesktopFolder | Возвращает интерфейс IShellFolder объекта-папки "Рабочий стол" (Desktop);
SHGetSpecialFolderLocation | Возвращает указатель на список идентификаторов специального объекта-папки.
SHBrowseForFolder | Проводит диалог с пользователем и возвращает указатель на список идентификаторов выбранного пользователем объекта-папки;
SHGetSpecialFolderPath | Версия 4.71. Возвращает путь файловой системы для специального объекта-папки. Функция предназначена для работы со специальными папками, а не для работы с виртуальными. При отсутствии нужной папки может, по требованию приложения, её создавать.

Навигация по пространству имён

Каждый объект-папка прдоставляет Вам возможность перебора всех объектов, которыми данный объект владеет. Для этого Вам предоставляется метод EnumObjects интерфейса IShellFolder, который возвращает интерфейс-итератор IEnumIDList. При этом Вы можете ограничить список (включать папки, не папки, скрытые и системные объекты).

Описание методов интерфейса IEnumIDList:
Clone | Создаёт новый объект-итератор, идентичный данному;
Next | Восстанавливает указанное количество идентификаторов элементов, находящихся в папке;
Reset | Возвращает итератор к началу последовательности;
Skip | Пропускает указанное количество элементов;

Таким образом Вы сможете получить набор указателей на списки идентификаторов, причём эти списки будут относительными по отношению к папке-владельцу.

Чтобы получить интерфейс IShellFolder для любого из этих объектов, Вам потребуется осуществить привязку, вызвав метод BindToObject интерфейса IShellFolder папки-владельца.

Чтобы узнать атрибуты данного объекта или нескольких объектов, необходимо вызвать метод GetAttributesOf интерфейса IShellFolder папки-владельца. При этом перед вызовом этого метода необходимо установить те атрибуты, значения которых Вы бы хотели выяснить. Если запрошены атрибуты нескольких элементов, то метод вернёт только те значения атрибутов, которые совпадают у всех переданных элементов. В частности, Вы сможете взять интерфейс IShellFolder только от тех объектов, которые имеют атрибут SFGAO_FOLDER. Вы можете обновить информацию об элементах, входящих в папку, использовав флаг SFGAO_VALIDATE.

Дополнительные возможности

Прежде всего, Ваше приложение всегда можете получить строку с именем объекта, представленном в удобном для Вас формате. Для этого интерфейс IShellFolder предоставляет метод GetDisplayNameOf.

Вы можете указать один из следующих требующихся форматов:

SHGDN_NORMAL | Обычный формат представления;
SHGDN_INFOLDER | Формат представления относительно данной папки;
SHGDN_INCLUDE_NONFILESYS | Приложение заинтересовано в именах элементов всех типов. Если этот флаг не установлен, то приложение заинтересовано лишь в тех элементах, которые представляют часть файловой системы. Если этот флаг не установлен, и элемент не представляет собой часть файловой системы, то этот метод может быть выполнен неудачно;
SHGDN_FORADDRESSBAR | Имя будет использовано для показа в адресном комбобоксе;
SHGDN_FORPARSING | Формат представления, используемый для дальнейшего разбора имени;

Имя элемента, полученное с установленным флагом SHGDN_FORPARSING, имеет особое значение. Вы можете использовать такое имя как командную строку для запуска приложения. Говоря точнее - такое имя эквивалентно понятию пути файловой системы.

Интерфейс IShellFolder предоставляет метод SetNameOf, позволяющий изменить экранное имя файлового объекта или вложенной папки. Изменяя экранное имя элемента, Вы изменяете его идентификатор, поэтому функция возвращает PIDL-указатель на новый идентификатор. Изменение экранного имени файлового объекта приводит к его фактическому переименованию в файловой системе.

Интерфейс IShellFolder также предоставляет метод ParseDisplayName, который позволяет узнать идентификатор элемента по его имени. Этому методу необходимо передавать имя, сгенерированное методом GetDisplayNameOf с установленным флагом SHGDN_FORPARSING.

С помощью глобального метода SHGetPathFromIDList по списку идентификаторов, определяющих объект относительно корня пространства имён, можно определить путь к объекту файловой системы.

С помощью глобального метода SHAddToRecentDocs Ваше приложение может добавить документ к списку последних, с которыми работал пользователь, или очистить этот список.

С помощью глобального метода SHEmptyRecycleBin, появившегося в версии 4.71 оболочки Windows, Ваше приложение может очистить корзину (Recycle Bin). Удаление файла в корзину (то есть - с возможностью дальнейшего восстановления) производится глобальным методом SHFileOperation, подробное описание которого выходит за рамки этого обзора. Вы также можете узнать количество объектов, расположенных в корзине, и их суммарный размер, с помощью метода SHQueryRecycleBin.

Примечания

Microsoft Visual C++ поставляется с файлами заголовков <comip.h> и <comdef.h> поддержки COM, в которых определены шаблон класса _com_ptr_t, инкапсулирующий функциональность ссылки на COM-объект, и самые распространённые специализации этого шаблона (в том числе для большинства стандартных интерфейсов пространства имён оболочки). При их использовании освобождение ссылок автоматизируется.

Copyright 1999 by Akzhan Abdulin. При публикации просьба указывать источник и авторство.

Комментарии, исправления, замечания и пожелания приветствуются по адресу: akzhan@beep.ru.

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

Q| У меня одна проблема: Пишу одну программку (написал уже довольно много) используя Win32API. И у меня возникла проблема со ScrollBar'ами. Вся загвоздка в том, что позиция бегунка прокрутки описана как short int и соответственно лежит в двухбайтном диапазоне. А в моей программе диапазон прокрутки может быть больше чем 32767. В хелпе на сообщение WM_VSCROLL советуют использовать функцию GetScrollPos, у меня че-то не получилось ее использовать. Как решить эту проблему? - Алексей Иванов

|A1 При работе с 32-битными значениями позиции бегунка значение nPos, передаваемое обработчику OnHScroll некорректно, для получения реального значения можно использовать ф-цию GetScrollPos, либо GetScrollInfo [возвращается в scrollinfo.nPos]. Однако, при обработке случая nSBCode==SB_THUMBTRACK, правильное значение текущей позиции возможно получить лишь при вызове GetScrollInfo(). Это значение будет возвращено в поле scrollinfo.nTrackPos;
Для работы с 32-битными значениями могут быть использованы следующие ф-ции: SetScrollPos, SetScrollRange, GetScrollPos, и GetScrollRange, SetScrollInfo, GetScrollInfo
Это все работает - лично проверял. - Bad Sector

|A2 Из вопроса не ясно, использует ли автор стили WS_HSCROLL и WS_VSCROLL или элемент управления scroll bar. Однако и в том, и в другом случае можно использовать одни и те же функции (SetScrollInfo/GetScrollInfo), чтобы управлять полосой прокрутки. Сначала (с помощью SetScrollInfo) для неё задаются 32-разрядные значения основных параметров (положения ползунка, диапазона изменения его положения и размера страницы). Затем в обработчике сообщений WM_VSCROLL и WM_HSCROLL можно использовать GetScrollInfo, чтобы получить значения всех этих параметров (опять же 32-разрядные!). Допустим, окно имеет стиль WS_VSCROLL. Тогда в указанные функции нужно передавать HWND этого окна и константу SB_VERT (в случае с горизонтальной полосой прокрутки используется SB_HORZ). Например:

// Создаём окно и инициализируем scroll bar.
HWND hWnd = CreateWindow(...);

SCROLLINFO si;
si.cbSize = sizeof(si);
si.fMask = SIF_POS | SIF_RANGE | SIF_PAGE;
si.nMin = 0;
si.nMax = 100000;   // больше, чем вмещает short!
si.nPos = 0;
si.nPage = 100;

SetScrollInfo(hWnd, SB_VERT, &si, TRUE);

...

LRESULT CALLBACK
WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
 switch (message)
 {
 // Обрабатываем сообщение WM_VSCROLL.
 case WM_VSCROLL:
 {
  SCROLLINFO si;
  si.cbSize = sizeof(si);
  si.fMask = SIF_PAGE | SIF_POS | SIF_RANGE | SIF_TRACKPOS;
  GetScrollInfo(hWnd, SB_VERT, &si);

  int pos = si.nPos;
  switch(LOWORD(wParam))
  {
   case SB_LINEUP:
    pos--;
    break;

   case SB_LINEDOWN:
    pos++;
    break;

   case SB_PAGEUP:
    pos -= si.nPage;
    break;

   case SB_PAGEDOWN:
    pos += si.nPage;
    break;

   case SB_TOP:
    pos = si.nMin;
    break;

   case SB_BOTTOM:
    pos = si.nMax;
    break;

   case SB_THUMBPOSITION:
    pos = si.nTrackPos;
    break;
  }

  // Устанавливаем новое положение ползунка.
  SetScrollPos(hWnd, SB_VERT, pos, TRUE);
  break;
 }

 ...

 }

   return DefWindowProc(hWnd, message, wParam, lParam);
}
В том случае, когда вместо стилей WS_xSCROLL используется элемент управления scroll bar, код выглядит совершенно аналогично, но функциям SetScrollInfo, GetScrollInfo и пр. передаётся HWND самой полосы прокрутки (а не владеющего ею окна), а в качестве второго параметра передаётся SB_CTL.
- Александр Шаргин (rudankort@mail.ru)

/ / / В ПОИСКАХ ИСТИНЫ / / / / / / / / / / / / /

Q| Как сделать так, чтобы программа сама себя могла стереть, т.е. свой *.exe файл? - LowFeaR

Ответить на вопрос

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Это все на сегодня. Пока!

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


No.32

Выпуск No. 32 от 11 февраля 2001 г.

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

/ / / / СТАТЬЯ / / / / / / / / / / / / / / / / / / / / / /

Автоматизация и моторизация приложения
 Акт первый

Автор: Николай Куртов
Редактор журнала СофтТерра
Софт Терра: Технологии Microsoft для разработчиков

Интро

Неслучайно именно эта статья была выбрана мной для начала рубрики, посвященной технологиям Microsoft для разработчиков. Слова OLE и COM (Component Object Model) на устах программистов вот уже 5 лет, тем не менее, парадигма компонентного подхода остается базовым и неизменным моментом в создании приложений. Я помню, насколько широкие горизонты я для себя открыл, осознав идею объектного подхода - с тех пор строю свои программы из компонентов-кирпичиков, объединяя их в более абстрактные модели - сервисы. Программирование давно стало сплавом творчества и строительства, оставляя в прошлом сугубо научно-шаманскую окраску ремесла. И если такой переход уже сделан, то сейчас можно обозначить новый виток - ломание барьеров API и переход к более обобщенному подходу в проектировании, выход на новый уровень абстракции. Немало этому способствовал интернет и его грандиозное творение - XML. Сегодня ключ к успеху приложения сплавляется из способности его создателей обеспечить максимальную совместимость со стандартами и в то же время масштабируемость. Придумано такое количество различных технологий для связи приложений и повторного использования кода, что сегодня прикладные программы не могут жить без такой "поддержки". Под термином "автоматизация" я понимаю настоящее оживление приложений, придание им способности взаимодействовать с внешней средой, предоставление пользователю максимального эффекта в работе с приложениями. Не равняясь на такие гранды технической документации, как MSDN, я, тем не менее, этой статьей хочу указать на путь, по которому сегодня проектируются современные приложения.

Автоматизация как есть

Автоматизация (Automation) была изначально создана как способ для приложений (таких как Word или Excel) предоставлять свою функциональность другим приложениям, включая скрипт-языки. Основная идея заключалась в том, чтобы обеспечить наиболее удобный режим доступа к внутренним объектам, свойствам и методам приложения, не нуждаясь при этом в многочисленных "хедерах" и библиотеках.

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

Для начала обратим внимание на самое дно - интерфейсы COM. Если термин "интерфейс" в этом контексте вам ничего не говорит, то представьте себе абстрактный класс без реализации - это и есть интерфейс. Реальные объекты наследуются от интерфейсов. Компоненты, наследующиеся от интерфейса IUnknown, называются COM-объектами. Этот интерфейс содержит методы подсчета ссылок и получения других интерфейсов объекта.

Автоматизация базируется на интерфейсе IDispatch, наследующегося от IUnknown. IDispatch позволяет запускать методы и обращаться к свойствам вашего объекта через их символьные имена. Интерфейс имеет немного методов, которые являются тем не менее довольно сложными в реализации. К счастью, существует множество шаблонных классов, предлагающих функциональность интерфейса IDispatch, поэтому для создания объекта, готового к автоматизации, необходимо весего лишь несколько раз щелкнуть мышкой в ClassWizard Visual C++.

Что касается способа доступа и динамического создания ваших внутренних dispatch объектов, то тут тут тоже все довольно просто - данные об объекте хранятся в реестре под специальным кодовым именем, которое называется ProgId. Например, progid программы Excel - Excel.Application. Cоздать в любой процедуре на VBScript достаточно легко - надо только вызвать функцию CreateObject, в которую передать нужный ProgID. Функция вернет указатель на созданный объект.

А как оно в MFC

В MFC существует специальный класс, под названием CСmdTarget. Наследуя свои классы от CСmdTarget, вы можете обеспечить для них необходимую функциональность в dispatch виде - как раз как ее понимают скрипты. При созднании нового класса в ClassWizard (View > ClassWizard > Add Class > New), наследуемого от CСmdTarget, просто щелкните на кнопке Automation или Creatable by ID, чтобы обеспечить возможность создания экземпляра объекта по его ProgID. Замечу, что для программ, реализующих внутреннюю автоматизацию, это не нужно. Для приложений, реализующих внешнуюю и смешанную автоматизацию, это необходимо для "корневых" объектов.

После создания такого объекта, ClassWizard создает интерфейс ITestAutomatedClass (это dispatch интерфейс, т.е. наследуется от IDispatch), который реализуется моим CTestAutomatedClass. Теперь к этому интерфейсу я могу добавить методы или свойства, которые автоматически будут реализованы в CTestAutomatedClass. Я добавил свойство Age.

COM-объекты, коим и является наш CTestAutomatedClass, можно создавать только динамически. Это связано с тем, что объект может использоваться несколькими приложениями одновременно, а значит, удаление объекта из памяти не может выполнить ни одно из них. Разумно предположить, что объект сам должен отвечать за свое удаление. Такой механизм реализован при помощи механизма ссылок (reference count). Когда приложение получает указатель на объект, он увеличивает свой внутренний счетчик ссылок, а когда приложение освобождает объект - счетчик ссылок уменьшается. При достижении счетчиком нуля, объект удаляет сам себя. Если наш объект был создан по ProgID другим приложением, то программа CTestApp (другими словами, Automation-Server) не завершится до тех пор, пока счетчик ссылок CTestAutomatedClass не станет равным нулю.

Создаваемые через ProgID COM-объекты, обычно являются Proxy-компонентами. Реально они не содержат никакой функциональности, но имеют доступ к приложению и его внутренним, не доступным извне, функциям. Хотя можно организовать все таким образом, чтобы всегда создавался только один COM-объект, а все остальные вызовы на создание возвращали указатели на него.

Метод интерфейса CCmdTarget GetIDispatch(), позволяет получить указатель на реализованный интерфейс IDispatch. В параметрах можно указать, нужно ли увеличивать счетчик ссылок или нет.

В следующей статье, посвященной использованию функциональности WebBrowser Control, я обращусь к практическому применению dispatch-объектов программы в скриптах. А в дальнейшем, мы поговорим о внедрении процессора скриптов в собственные приложения.

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

Q| Как сделать так, чтобы программа сама себя могла стереть, т.е. свой *.exe файл? - LowFeaR

|A1 Удалить программу в тот момент, когда она запущена, не представляется возможным (во всяком случае такая возможность мне не знакома), остается удаление после завершения ее выполнения. Идея следующая: при выходе из программы создать BAT-файл, который ждет до тех пор, пока файл можно будет удалить (программа завершит работу), удаляет файл программы и себя, и запустить его:

void MyDlg::OnDestroy()
{
        CDialog::OnDestroy();

        const char *AppName=AfxGetApp()->m_pszExeName;
        FILE *f=fopen("selfdel.bat","w+");
        fprintf(f,
                ":dc\n"
                "del %s.exe\n"
                "if exist %s.exe goto dc\n"
                "del selfdel.bat",AppName,AppName);
        fclose(f);
        WinExec("selfdel.bat",FALSE);
}

Преимущества:
-файл удаляется сразу в тот момент, когда это становится возможно

Недостатки:
-если запустить два экземпляра приложения, то после завершения работы первого мы получаем цикл активного ожидания до тех пор пока не завершится второй экземпляр (это незаметно в W95/98, но в NT в окне Task Manager можно заметить полную загрузку процессора). Также пользователь все это время будет удивляться наличию невесть откуда взявшегося файла sefdel.bat

Майкрософт же предлагает свой способ решения проблемы, причем его реализация отличается для WinNT и Win95/98. Удаление (переименование, замещение, и т.д.) файла происходит во время следующей перезагрузки системы.

Win95/98: В процессе перезагрузки системы запускается утилита wininit.exe, которая осуществляет заданные действия над файлами, указанные в секции [rename] файла wininit.ini. При этом т.к. wininit.exe запускается еще до того как запущена система поддержки длинных имен файлов, все имена должны быть указаны в формате DOS (8.3).

Последовательность действий для удаления или переименования файла:
1.Проверить наличие файла WININIT.INI в директории Windows
2.Если WININIT.INI существует, открываем его и добавляем новые
строки в секцию [rename]. Если файла нет, создаем его и секцию [rename] в нем. 3.Добавляем строки следующего формата в секцию [rename]:
DestinationFileName=SourceFileName
Оба пути DestinationFileName и SourceFileName не должны содержать длинных имен. Приемник и источник должны находится на одном диске. Для удаления файла вместо DestinationFileName использовать NUL.

WinNT:
Здесь все сделано по-человечески. Для удаления файла следует использовать функцию MoveFileEx():
MoveFileEx(szSrcFile, NULL, MOVEFILE_DELAY_UNTIL_REBOOT);
где szSrcFile - имя файла или директории
Преимущества:
-"Лицензированный" метод Майкрософт
Недостатки:
-Чрезмерно утяжеленная процедура редактирования wininit.ini, проблемы при работе с длинными именами Win95/98,
-Удаление происходит только в момент перезагрузки. - Bad Sector

|A2 Программа не может удалить свой exe-файл, пока она работает. Это фундаментальное правило при работе под Windows. Поэтому всё, что остаётся - это поручить удаление другому процессу перед тем как завершить свой.

Самый простой вариант - создать на лету и запустить bat-файл, который дождётся завершения нашего процесса, а затем удалит его exe-файл. Более сложные варианты подразумевают создание в чужом процессе (например, в Task Manager) рабочего потока, который опять же дождётся завершения нашего процесса и убьёт файл.

Вот пример функции, которая создаёт bat-файл и запускает его, чтобы убить наш exe-файл. Лучше всего вызывать её непосредственно перед завершением нашего процесса.

void DelSelf()
{
 // Получаем свой путь
 char szExePath[MAX_PATH];
 GetModuleFileName(NULL, szExePath, MAX_PATH);

 // Создаём bat-файл
 static char szBat[] =
  ":Loop\r\n"
  "del %1\r\n"
  "if exist %1 goto Loop\r\n"
  "del %0";

 HANDLE hFile = CreateFile("__delself.bat", GENERIC_WRITE, 0, NULL,
CREATE_ALWAYS, 0, 0);
 DWORD temp;
 WriteFile(hFile, (LPVOID)szBat, strlen(szBat), &temp, NULL);
 CloseHandle(hFile);

 // Запускаем его
 STARTUPINFO si;
 ZeroMemory(&si, sizeof(si));
 si.cb = sizeof(si);
 si.wShowWindow = SW_HIDE;
 si.dwFlags = STARTF_USESHOWWINDOW;

 PROCESS_INFORMATION pi;

 char szCommand[MAX_PATH+15] = "__delself.bat ";
 strcat(szCommand, szExePath);
 CreateProcess(NULL, szCommand, NULL, NULL, FALSE, DETACHED_PROCESS, NULL,
NULL, &si, &pi);

 return;
}

Замечу, что это только пример, который можно улучшать в различных направлениях. Можно, скажем, получать имя bat-файла через GetTempFileName, чтобы гарантировать его уникальность. Или понизить приоритет создаваемого из bat-файла процесса до минимума, чтобы он кушал поменьше ресурсов в процессе циклической проверки существования exe-файла. - Александр Шаргин (rudankort@mail.ru)

/ / / В ПОИСКАХ ИСТИНЫ / / / / / / / / / / / / /

Q| Есть у меня файлы с расширением .pdb (Microsoft C/C++ program database 2.00)(их MS VC++ делает в папке Debug проекта создаются), можно ли с их помощью восстановить исходники программы (размер у них такой, что туда не только прога влезет, но и комментарии к ней в HTML (FrontPage Style) формате :) - Andrey Shtukaturov

Ответить на вопрос

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Пока все. До скорого!

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


No.33

Выпуск No. 33 от 18 февраля 2001 г.

Приветствую!

Сегодня хочу предложить вашему вниманию заключительную часть статьи про ODBC нашего постоянного автора Александра Шаргина, которая, если вы помните, открывает цикл статей о технологиях доступа к данным.

/ / / / СТАТЬЯ / / / / / / / / / / / / / / / / / / / / / /

Доступ к БД с использованием ODBC
 Часть 2

Автор: Александр Шаргин

В предыдущей части статьи мы с вами рассмотрели основы использования ODBC для доступа к базам данных. Но в процессе знакомства с этой технологией у каждого естественным образом возникает целый ряд вопросов. Ответам на самые распространённые из них и будет посвящена вторая часть статьи.

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

Необходимые компоненты
Сразу замечу, что некоторые приложения изначально разрабатываются для работы с произвольными БД. К таким приложениям относятся пакеты статистической обработки данных или электронные таблицы, способные импортировать данные из выбранной пользователем БД. Другой характерный пример - среда Visual C++. Если вы разрабатываете подобное приложение, можете смело пропустить этот раздел. Установка компонентов, необходимых для работы с конкретной базой данных - не ваша забота. Любому приложения, использующему ODBC, необходимы основные компоненты ODBC (core components) и ODBC-драйвер. К основным компонентам относятся менеджер драйверов (ODBC32.DLL), библиотека инсталлятора (ODBCCP32.DLL), библиотека курсоров (ODBCCR32.DLL) и администратор источников данных (ODBCAD32.EXE), а также несколько вспомогательных файлов. Драйвер состоит из двух DLL: библиотеки драйвера (driver DLL) и библиотеки настройки (setup DLL). Библиотека драйвера экспортирует все необходимые функции ODBC API, а библиотека настройки - функции ConfigDriver и ConfigDSN, используемые для конфигурирования самого драйвера и связанных с ним источников данных. Иногда обе библиотеки объединяют в одной DLL. Основные компоненты сейчас установлены практически на каждом компьютере, поэтому об их инсталляции я рассказывать не буду. Тем, кого интересует этот вопрос, советую обратиться к описанию функции SQLInstallDriverManager. Драйвер для каждой конкретной СУБД обычно распространяется со своей программой инсталляции. В этом случае вам нужно просто включить её в комплект поставки. Но предположим, что такая программа недоступна. Тогда можно воспользоваться функцией SQLInstallDriverEx, входящей в библиотеку инсталляции. Эта функция вызывается дважды: первый раз, чтобы определить целевую папку для драйвера, а второй раз, чтобы добавить необходимые записи в реестр. Копирование осуществляет вызывающая программа. Предположим, что драйвер "My Driver" состоит из файлов MYDRV.DLL и MYSETUP.DLL. Установку этого драйвера выполнит следующий код.

#include <windows.h>
#include <odbcinst.h>
#include <stdio.h>
:

char        szPathIn[301];
char        szPathOut[301];
DWORD      dwUsageCount;
int         i, j;

char szDriver[300] = "My Driver\0Driver=MYDRV.DLL\0Setup=MYSETUP.DLL\0";

SQLInstallDriverEx(
                 szDriver,
                 NULL,      szPathIn,
                 300, NULL,
                 ODBC_INSTALL_INQUIRY,
                &dwUsageCount);

// Копируем файлы в папку szPathIn.

sprintf(szDriver,
          "My Driver;Driver=%s\\%s;Setup=%s\\%s;",
          szPathIn, "MYDRV.DLL", szPathIn, "MYSETUP.DLL");

for (i = strlen(szDriver), j = 0; j < i; j++)
{
      if (szDriver[j] == ';')
            szDriver[j] = '\0';
}

SQLInstallDriverEx(
            szDriver,
            szPathIn, szPathOut,
            300, NULL,
            ODBC_INSTALL_COMPLETE,
            &dwUsageCount);

Если в дальнейшем вам потребуется удалить установленный таким образом драйвер, сделать это можно так.

DWORD dwUsageCount;
SQLRemoveDriver("My Driver", TRUE, &dwUsageCount);

Как и в случае с SQLInstallDriverEx, физическое удаление файлов остаётся на вашей совести. Удалять файл следует только если счётчики использования как компонента, так и самого файла равны нулю.

За подробностями об установке компонентов ODBC следует обратиться к главам 18 и 23 из ODBC Programmer's Reference.

Программная регистрация источника данных
Для программной регистрации источника данных используется функция SQLConfigDataSource. Вызывайте её с ключом ODBC_ADD_DSN, чтобы создать пользовательский источник данных, или с ключом ODBC_ADD_SYS_DSN для создания системного источника данных.

Примечание: системный источник данных отличается от пользовательского тем, что он доступен всем пользователям компьютера, в то время как пользовательский доступен только создавшему его пользователю. Соответственно, информация о системных источниках данных хранится в реестре в разделе HKEY_LOCAL_MACHINE, а о пользовательских - в разделе HKEY_CURRENT_USER.

Функция SQLConfigDataSource получает имя драйвера, а также набор параметров, описывающих создаваемый источник данных. Состав этих параметров изменяется от драйвера к драйверу. Например, драйверу MS Access нужно сообщить, по крайней мере, имя источника данных (DSN) и имя файла БД (DBQ). Вот как выглядит создание источника данных dbFolks для БД :

SQLConfigDataSource(
            NULL, ODBC_ADD_DSN,
            "Microsoft Access Driver (*.mdb)",
            "DSN=dbFolks\0DBQ=c:\\folks.mdb");

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

Как обойтись без источника данных
Хотя источники данных являются удобной абстракцией, иногда хочется обойтись без них. Как это сделать? ODBC не предоставляет стандартной возможности подключаться напрямую к БД. Как мы помним, стандарт определяет только четыре параметра (DSN, UID, PWD и DRIVER) для строки подключения, передаваемой в CDatabase::OpenEx. Среди них нет параметра, в который можно было бы записать имя конкретной БД. Тем не менее, стандарт не запрещает драйверам ODBC распознавать и другие параметры. Используя их, вы жертвуете универсальностью вашей программы, но взамен получаете доступ к дополнительным возможностям драйвера.

В частности, многие драйверы от Microsoft используют параметр DBQ для задания имени файла БД. Вот как можно подключиться к БД , не создавая для неё источника данных.

CDatabase Db;

Db.OpenEx(
      "DRIVER={Microsoft Access Driver (*.mdb)};DBQ=f:\\folks.mdb",
      CDatabase::noOdbcDialog);

Точно так же можно подключиться к БД, управляемой MS SQL Server, используя параметры SERVER и DATABASE. Вот пример подключения к демонстрационной БД , поставляемой вместе с SQL Server.

CDatabase Db;

Db.OpenEx(
     "DRIVER={SQL Server};SERVER=(local);DATABASE=pubs;UID=sa;PWD=",
     CDatabase::noOdbcDialog);

Описание дополнительных параметров следует искать в документации на каждый конкретный драйвер. В частности, информация о драйверах фирмы Microsoft содержится в MSDN.

Начинаем с нуля
До сих пор мы обсуждали работу с уже существующими базами данных. Но иногда возникает необходимость создать базу данных или отредактировать её структуру <на лету>, в процессе работы вашей программы. О том, как это сделать, и пойдёт речь в этом разделе.

Создание БД
Хотя в ODBC нет стандартных средств для создания новых баз данных, некоторые драйвера предоставляют такую возможность. Например, драйвер MS Access распознаёт дополнительные параметры для уже знакомой нам функции SQLConfigDataSource. Один из них, CREATE_DB, как раз и служит для создания новых баз данных. Для примера рассмотрим создание БД .

SQLConfigDataSource(
            NULL, ODBC_ADD_DSN,
            "Microsoft Access Driver (*.mdb)",
            "CREATE_DB=c:\\folks.mdb");

Обратите внимание, что никакого источника данных в этом случае не создаётся, хотя мы и обращаемся к SQLConfigDataSource.

В некоторых случаях драйвер не поддерживает создания БД, но в диалекте SQL соответствующей СУБД есть необходимые для этого конструкции. В этом случае можно использовать функцию CDatabase::ExecuteSQL для выполнения требуемых операторов языка SQL. Для примера рассмотрим, как создаётся новая база данных в SQL Server.

Db.OpenEx(
         "DRIVER={SQL Server};SERVER=(local);UID=sa;PWD=",
          CDatabase::noOdbcDialog);
Db.ExecuteSQL("CREATE DATABASE MyDB");

Если ни один из перечисленных способов создания БД вам не доступен, всё, что вам остаётся - это распространять вместе с вашей программой пустую БД и копировать её каждый раз, когда требуется создать новую базу данных.

Создание таблиц
Для создания таблиц в БД используется SQL-оператор CREATE TABLE, который выполняется с помощью функции CDatabase::ExecuteSQL. В простейшем случае CREATE TABLE имеет следующий формат.

CREATE TABLE table_name
(
   {column_name column_type} [,:n]
)

Полное описание формата CREATE TABLE для каждой конкретной СУБД можно найти в документации. Рассмотрим пример создания таблицы tPeople, содержащей поля Name (строка из 50 символов) и DateOfBirth (дата). Запрос будет выглядеть так.

Db.ExecuteSQL("CREATE TABLE tPeople (Name char(50), DateOfBirth datetime)");

Модификация полей (столбцов)
Иногда приходится не создавать таблицу с нуля, а модифицировать уже существующую. Не останавливаясь на подробностях, скажу, что для этого используется SQL-оператор ALTER TABLE, с помощью которого можно как добавлять в таблицу новые поля, так и изменять или удалять существующие.

Рассмотрим несколько примеров. Сначала добавим в таблицу tPeople поле DateOfDeath (дата).

Db.ExecuteSQL("ALTER TABLE tPeople ADD DateOfDeath datetime");

Теперь изменим ширину поля Name (с 50 до 100 символов).

Db.ExecuteSQL("ALTER TABLE tPeople ALTER COLUMN Name char(100)");

А теперь удалим только что созданное поле DateOfDeath:

Db.ExecuteSQL("ALTER TABLE tPeople DROP COLUMN DateOfDeath");

В заключение замечу, что SQL предоставляет вам практически неограниченные возможности по манипулированию БД. Если вам не удаётся найти функции ODBC, выполняющей требуемое действие, попробуйте найти подходящий SQL-оператор и выполнить его с помощью CDatabase::ExecuteSQL.

Работа в незнакомой обстановке
Как я уже говорил, иногда на этапе разработки приложения точно не известно, с какой базой данных ему предстоит работать. В таком случае вам понадобятся средства для поиска доступных источников данных, а также для динамического определения структуры БД.

В ODBC есть целый набор похожих функций, предназначенных для получения списка доступных драйверов и источников данных, таблиц в БД и столбцов в таблице. Они называются SQLDrivers, SQLDataSources, SQLTables и SQLColumns соответственно. Обратите внимание, что это функции ODBC API, для которых не существует обёртки в MFC.

Кроме того, в класс CRecordset встроены функции GetODBCFieldCount и GetODBCFieldInfo. Первая возвращает количество полей (столбцов) в наборе записей, а вторая заполняет структуру CODBCFieldInfo информацией о заданном поле.

Хранимые процедуры
Хранимая процедура - это сценарий на языке SQL, который вызывается клиентом для выполнения некоторых операций и работает на стороне сервера. Хранимые процедуры могут получать входные параметры, а также сообщать о результатах своей работы, возвращая наборы записей или записывая некоторые значения в выходные параметры. Работе с хранимыми процедурами и посвящён данный раздел.

Вызов процедур
Хранимая процедура вызывается при помощи SQL-оператора CALL. Обратите внимание, что использование этого оператора является обязательным требованием к ODBC-программе, даже если СУБД поддерживает другой оператор вызова процедур (например, EXEC[UTE] в SQL Server).

Выполнение оператора CALL осуществляется с помощью функции CDatabase::ExecuteSQL. Сам оператор заключается в фигурные скобки. Рассмотрим пример вызова процедуры spClear, не требующей параметров (она очищает таблицу tPeople, выполняя оператор DELETE * FROM tPeople).

Db.ExecuteSQL("{CALL spClear}");

Вызов процедур с параметрами
Вызов хранимой процедуры с параметрами осуществляется при помощи той же команды, но после имени процедуры в скобках через запятую записывается список параметров.

Модифицируем хранимую процедуру из предыдущего примера так, чтобы она принимала параметр paramName и удаляла из таблицы tPeople только людей с заданным именем (то есть выполняла оператор DELETE * FROM tPeople WHERE Name=paramName). Теперь мы можем удалить из таблицы всех Александров, выполнив:

Db.ExecuteSQL("{CALL spClear('Alexander')}");

Это наиболее простой способ, но он имеет ряд ограничений. В частности, невозможно получить доступ к выходным параметрам функции. Чтобы снять эти ограничения, необходимо связать нужные нам параметры с переменными. Связывание производится в функции CDatabase::BindParameter, которую для этой цели нужно перегрузить. Это, в свою очередь, означает, что нам придётся порождать новый класс от CDatabase. Для связывания каждого параметра с переменной используется функция SQLBindParameters из ODBC API. Вместо каждого связанного с переменной параметра в вызов процедуры вставляется вопросительный знак.

Рассмотрим пример вызова хранимой процедуры spCount, которая возвращает количество людей с заданным именем в таблице tPeople. В СУБД SQL Server такую процедуру можно создать, выполнив запрос:

CREATE PROC spCount(@paramName CHAR(50), @paramCount INT OUTPUT)

AS SELECT @paramCount = COUNT(*) FROM tPeople WHERE Name=@paramName

Теперь, чтобы посчитать количество Александров, необходимо написать следующий код.

// Порождаем новый класс от CDatabase
class CMyDatabase : public CDatabase
{
public:
      char      m_paramName[50];
      int      m_paramCount;

      void BindParameters(HSTMT);
};

void CMyDatabase::BindParameters(HSTMT hStmt)
{
      SQLBindParameter(  // Связываем @paramName с m_paramName
          hStmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR,
          SQL_CHAR, 50, 0, m_paramName, 50, NULL);
      SQLBindParameter( // Связываем @paramCount с m_paramCount
          hStmt, 2, SQL_PARAM_OUTPUT, SQL_C_SLONG,
          SQL_INTEGER, 0, 4, &m_paramCount, 4, NULL);
}
:

CMyDatabase Db;

Db.OpenEx(
      "DRIVER={SQL Server};SERVER=(local);DATABASE=tPeople;UID=sa;PWD=",
       CDatabase::noOdbcDialog);

strcpy(Db.m_paramName, "Alexander");

Db.ExecuteSQL("{CALL spCount(?,?)}");
// Db.m_paramCount содержит результат!

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

Вызов процедур, возвращающих наборы записей
Процедуры, возвращающие наборы записей, также вызываются с помощью оператора CALL, но в функции CRecordset::Open. Соответственно, полученное множество записей будет связано с объектом класса CRecordset. Если у хранимой процедуры есть параметры, можно передать их напрямую или связать с ними переменные. Связывание переменных, в отличие от предыдущего случая, происходит в функции CRecordset::DoFieldExchange при помощи макросов RFX_* (то есть практически ни чем не отличается от связывания переменных с полями результирующего набора записей). Нужно только вызвать CFieldExchange::SetFieldType с параметром CFieldExchange::inputParam, чтобы сообщить MFC, что мы связываем параметры, а не поля. Важно также записать количество связываемых параметров в переменную CRecordset::m_nParams. Обычно это делается в конструкторе класса.

Рассмотрим пример вызова хранимой процедуры spGetByName, которая находит в таблице tPeople всех людей с заданным именем. В СУБД SQL Server такую процедуру можно создать, выполнив запрос:

CREATE PROC spGetByName(@paramName CHAR(50))

AS SELECT * FROM tPeople WHERE Name=@paramName

Построить набор записей, в который входят все Александры из таблицы, теперь можно так (напоминаю, что нам придётся порождать новый класс от CRecordset).

class CPeople : public CRecordset
{
  public:
     CPeople(CDatabase *pDatabase = NULL) :
     CRecordset(pDatabase) { m_nFields = 2, m_nParams = 1; };

     CString      m_Name;
     CTime       m_DateOfBirth;
     CString      m_paramName;

     void DoFieldExchange(CFieldExchange *pFX);
};

void CPeople::DoFieldExchange(CFieldExchange *pFX)
{
     pFX->SetFieldType(CFieldExchange::outputColumn);
     RFX_Text(pFX, "Name", m_Name, 50);
     RFX_Date(pFX, "DateOfBirth", m_DateOfBirth);
     pFX->SetFieldType(CFieldExchange::inputParam);
     RFX_Text(pFX, "paramName", m_paramName);
}

:

CPeople Rs(&Db);
Rs.m_paramName = "Alexander";
Rs.Open(CRecordset::snapshot, "{CALL spGetByName(?)}");

О чём ещё полезно знать
В заключительном разделе я рассмотрю несколько не связанных между собою тем, знакомство с которыми может оказаться полезным.

Транзакции
Транзакция - это блок команд, которые выполняются как единое целое. Другими словами, они либо выполняются все, либо не выполняется ни одна. Транзакция начинается вызовом CDatabase::BeginTrans и завершается вызовом CDatabase::CommitTrans. Все операции по изменению, добавлению и удалению данных вступят в силу только после вызова CommitTrans, причём в любой момент до вызова этой функции транзакцию можно полностью отменить, вызвав функцию CDatabase::Rollback. Используйте CDatabase::CanTransact, чтобы определить, поддерживает ли используемый вами драйвер транзакции.

CRecordset и его потомки
В первой части статьи мы рассмотрели, как использовать CRecordset, порождая от него новые классы. Возникает вопрос: а можно ли использовать этот класс напрямую? Ответ на этот вопрос звучит так: CRecordset может использоваться для доступа к множеству записей, построенному только на основе запроса (а не имени таблицы), и только в режиме read only. Обратиться к значениям конкретных полей в этом случае можно, используя функцию CRecordset::GetFieldValue. Функции CRecordset::Move* используются, как и раньше.

Следующий фрагмент выводит фамилии всех авторов из БД pubs. Так как нам требуется доступ к таблице authors в режиме <только для чтения>, мы можем использовать класс CRecordset напрямую.

CRecordset Rs(&Db);

Rs.Open(CRecordset::forwardOnly, "SELECT au_lname FROM authors");

while (!Rs.IsEOF())
{
     CString lname;

      Rs.GetFieldValue((short)0, lname);
      printf ("%s\n", lname);
      Rs.MoveNext();
}

Как обмануть IntelliSense
Мы уже умеем конструировать объекты класса CRecordset, передавая конструктору указатель на соединение:

CRecordset Rs(&Db);

Существует ещё одна эквивалентная форма создания объекта CRecordset:

CRecordset Rs;
Rs.m_pDatabase = &Db;

Зачем она может понадобиться, спросите вы. Дело в том, что система Microsoft IntelliSense, которая услужливо выдаёт вам списки членов класса и параметров функции прямо <на лету>, очень болезненно реагирует на конструкторы с параметром: в коде, который следует за вызовом такого конструктора, подсказки попросту перестают появляться. Если вы столкнулись с такой проблемой, смело используйте второй вариант конструирования объекта CRecordset.

Любые замечания по форме и содержанию статьи вы можете прислать мне по адресу rudankort@mail.ru.

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

Q| Есть у меня файлы с расширением .pdb (Microsoft C/C++ program database 2.00)(их MS VC++ делает в папке Debug проекта создаются), можно ли с их помощью восстановить исходники программы (размер у них такой, что туда не только прога влезет, но и комментарии к ней в HTML (FrontPage Style) формате :) - Andrey Shtukaturov

|A Восстановить исходники не удастся, так как их там нет. Зато есть имена классов и глобальных переменных.
Этого вполне достаточно для того чтобы восстановить недокументированный интерфейс COM-объекта.
Дело в том, что согласно реализации COM'а для С++, имена функций членов класса реализующиго интерфейс должны полностью совпадать с именами исходного интерфейса. Плюс свои какие-то матоды. Обычно они не виртуальные так что легко отсекаются.
В .pdb файлах лежат vftable для базовых интерфейсов, так что можно восстановить всю иерархию интерфейсов. И гадать в каком порядке методы в интерфейсе не придется
И собственные методы класса реализующего интерфейс тоже отсекаются.

Для не COM-объектов .pdb файлы тоже могут быть полезны.
Если, например, в перечне экспортируемых из ImgUtil.dll функций содержится скупое "DecodeImage", то в .pdb файле честно написано, что это "_DecodeImage@12", т.е. уже извесно количество параметров. Это для функций описанных как extern "C". Для функций C++ в .pdb файле будет полное задекорированное имя.
Типа
"?DecodeImage@@YAJPAVISniffStream@@PAVIMapMIMEToCLSID@@PAVIImageDecodeEventS ink@@@Z"
Что после пропускания через утилиту UndName из набора утилит поставляемого MS с PlatformSDK выглядит как
"long __cdecl DecodeImage(class ISniffStream *,class IMapMIMEToCLSID *,class IImageDecodeEventSink *)".
Более чем достаточно для восстановления не целиком исходников, но хоть декларации функций. - Paul Bludov

/ / / В ПОИСКАХ ИСТИНЫ / / / / / / / / / / / / /

Q| Насколько корректно будут работать методы контроля утечек памяти (в частности объект CMemoryState) в многопоточных приложениях?
У меня сложилось впечатление, что объект CMemoryState не делает различия в каком потоке вызывались операторы new с момента обращения к memState.Checkpoint() до обращения к memState.DumpAllObjectsSince().
Видимо "моментальные снимки" распределённой памяти в данном случае не информативны, ведь несколько потоков работают в одном адресном пространстве? - Николай Турпитко

Ответить на вопрос

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Это все на сегодня. Успехов!

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


No.34

Выпуск No. 34 от 25 февраля 2001 г.

Добрый день, уважаемые подписчики!

Многие из вас в своих письмах спрашивали о том, как можно включить функциональность Internet Explorer в свои приложения. На этот вопрос призвана ответить вторая часть статьи Николая Куртова, первая часть которой была опубликована в выпуске No.32.

/ / / / СТАТЬЯ / / / / / / / / / / / / / / / / / / / / / /

Автоматизация и моторизация приложения
 Акт второй

Автор: Николай Куртов
Редактор журнала СофтТерра
Софт Терра: Технологии Microsoft для разработчиков

Интро

Помните, какой революцией был Windows 95, с его новыми элементами: list view, tree view, sliders, tabs ? Радикально отличаясь от своего предшественника, он представлял дизайнерам пользовательского интерфейса новые гибкие возможности. Сегодня требования к программному обеспечению растут, информации становится больше, информация становится разнообразнее. Теперь, древовидными списками с закладками не обойтись. И вот, выходит Windows 98, где папки можно просматривать в режиме web, работая с наглядной информации. Круговая диаграмма, дополнительная информация о папке, Outlook today - все это на самом деле реализовано в HTML, а еще точнее, в DHTML (т.е. Dynamic HTML, оживший, при помощи скриптов, HTML). Все help системы Windows 98/2000 уже представлены в HTML виде.

Зачастую оказывается, что web-интерфейсы значительно дружественнее, чем обычные диалоговые окна, ведь они ориентированы больше на документ, нежели на приложение. Да и разработчику они обходятся дешевле, чем поддержание многозакладочных информационных диалогов. Дизайн приложений в стиле Web предлагает множество преимуществ, такие как богатая визуализация и концепция навигации через гиперссылки. Хорошо сконструированный пользовательский интерфейс не только приносит эстетическое удовлетворение, но и является ключом к успеху всего приложения.. И все это вызывает энтузиазм, до тех пор, пока дело не доходит до реализации. Красота дело тонкое, потому сегодня я попытаюсь рассказать о некоторых аспектах реализации web-интерфейсов. [...]

Как это работает

Internet Explorer (c версии 4.0 и позже) предоставляет технологии, при помощи которых программисты могут встраивать всю функциональность браузера в свои приложения. Эти технологии реализуются в ActiveX компонентах, как визуальных так и невидимых. Основной компонент, представляющий элемент web-browser control, содержится в библиотеке shdocvw.dll, использующей средства парсинга и рендринга HTML кода, а также выполнение DHTML скриптов от другого компонента - mshtml.dll. По сути, web-browser control является обычным ActiveX компонентом, с множеством стандартных свойств. Тем не менее, каждая загруженная страничка внутри такого элемента представляется в виде объектной модели документа HTML. Это значит, что любой элемент HTML, такой как параграф или ячейка таблицы, доступен разработчику в виде COM-объекта, со множеством свойств и методов.

По-правде говоря, библиотеки shdocvw.dll, а особенно mshtml.dll не такие уж и легковесные относительно памяти. Тем не менее следует учитывать, что обычно webbrowser control подгружается системой на запуске, а все повторные запросы на загрузку этих библиотек перенаправляются на уже загруженные ранее модули. Таким образом использование webbrowser control не влечет чрезмерного расходования системных ресурсов, если конечно, ваш html документ не имеет сверхсложной структуры и гигантстких размеров.

Internet Explorer версии 5.5 предоставляет поистине громадное количество новых возможностей для разработчика, что позволяет создавать мультимедийные системы на основе браузера. Подробное описание нововведений можно найти в последних выпусках MSDN.

Web browser control

Прежде, чем приступать к реализации, отмечу, что буду использовать в примерах классы MFC. Естественно, существует множество путей для внедрения web-компонента в приложения на Visual Basic, C++ ATL или Delphi. Я надеюсь, пользователи этих средств, найдут эту статью столь же полезной, сколь и пользователи MFC.

Вставка компонента

Использовать компонент можно "напрямую", вставляя OLE-объект на форму, или косвенно, через вызов к CWnd::CreateControl. Важным фактом является наличие уже созданной обертки для webbrowser в MFС, реализованной в классе CHTMLView. При создании приложений по схеме В, я рекомендую пользоваться именно им. Встроенные визарды Visual Studio уже содержат все средства для начальной генерации таких приложений. Ежели все-таки душе роднее тернистый путь, то внедрение компонента будет выглядит следующим образом:

CRect rectClient(10,10,200,200);
CWnd m_wndBrowser;
CComQIPtr<IWebBrowser2, &IID_IWebBrowser2> m_pBrowserApp;

if (!m_wndBrowser.CreateControl(CLSID_WebBrowser, _T("Window"),
 WS_VISIBLE | WS_CHILD, rectClient, this, AFX_IDW_PANE_FIRST))
{
 DestroyWindow();
}
if (m_pBrowserApp = m_wndBrowser.GetControlUnknown())
{
 CComBSTR bstrURL = _T("http://www.microsoft.com");
 m_pBrowserApp->Navigate(bstrURL, NULL, NULL, NULL, NULL);
}

Замечу, что CLSID_WebBrowser - идентификатор объекта webbrowser, описанный в файле comdef.h. Этот файл имеет ключевое значение, поскольку в нем отражены идентификаторы основных интерфейсов объектной модели Windows, в частности WebBrowser и объектной модели HTML. Для большинства элементов объявлены smart-pointers, что особенно актуально для работы с DHTML из приложения, где просто море различных интерфейсов. Помимо стандартных для ActiveX элементов интерфейса, webbrowser компонент экспортирует также два собственных интерфейса:

  • IWebBrowser2. Этот интерфейс реализует управление элементом: внешним видом, параметрами, а также позволяет производить навигацию.
  • DWebBrowserEvents2. Объект WebBrowser использует события для уведомления приложения о состоянии компонента. Например, перед навигацией на новый URL, вызывается событие BeforeNavigate2.

Описание этих интерфейсов exdisp.h/exdispid.h. Оглядываясь на практический опыт, замечу, что ссылки на все описанные файлы лучше прописывать в stdafx.h.

Подключение событий

Механизм подключения событий через точки соединения стандартный, поэтому не имеет смысла его здесь описывать. Тем более, что MFC предоставляет более удобный способ для отлова событий webbrowser через DECLARE_EVENTSINK_MAP макрос.

Запишем в заголовочном файле класса, содержащего webbrowser control:

// Web browser event sink
 DECLARE_EVENTSINK_MAP()
 virtual void OnDownloadComplete();
 virtual void DocumentComplete(LPDISPATCH pDisp, VARIANT* URL);

А в .cpp файле добавим строки:

BEGIN_EVENTSINK_MAP(CChatChannelDialog, CDialog)
ON_EVENT(CChatChannelDialog, AFX_IDW_PANE_FIRST, DISPID_NAVIGATECOMPLETE,
 OnDownloadComplete, VTS_NONE)
ON_EVENT(CChatChannelDialog, AFX_IDW_PANE_FIRST, DISPID_DOCUMENTCOMPLETE,
 DocumentComplete, VTS_DISPATCH, VTS_PVARIANT)
END_EVENTSINK_MAP()

Для полной реализации механизма отлова событий этим путем, лучше всего обратиться к исходным текстам CHTMLView.

Модель объектов DHTML

Интерфейс DWebBrowserEvents2 при помощи события DISPID_NAVIGATECOMPLETE позволяет определить тот момент, когда HTML документ полностью сгенерирован внутри webbrowser control. После того, как это происходит, весь HTML документ доступен через функцию IWebBrowser2::get_Document. Также, как и webbrowser control, HTML документ поддерживает события, такие как click, mouseover. Для того, чтобы использовать объектную модель DHTML, нужно подключить заголовок mshtml.h.

CComQIPtr<IHTMLDocument2,&IID_IHTMLDocument2> pADocument;
IDispatch* pdispTmpVal;

m_pBrowserApp->get_Document(&pdispTmpVal);
pADocument = pdispTmpVal;
pdispTmpVal->Release();

Интерфейс IHTMLDocument2 предоставляет возможность получать и модифицировать содержимое документа. Вы можете использовать множество методов, таких как get_body, get_all, get_activeElement чтобы извлекать элементы или коллекции элементов внутри документа. Базовой основой для любого тэга внутри HTML-документа является интерфейс IHTMLElement. Меняя содержимое тэга при помощи свойств innerHTML и outerHTML мы реализуем принцип динамического содержания, который нами и преследовался. К любому элементу можно адресоваться при помощи идентификатора id через вызов IHTMLElementCollection::Item. Итак, c визуализацией ясно, а как же теперь обеспечить интерактивность? Как избавиться от ненужных клавишных комбинаций и меню? Как получить доступ из скриптов к внутренней модели объектов нашей программы?

Расширение объектной модели DHTML

Компания Microsoft предоставила возможность расширения объектной модели через механизм window.external. Приложение, использующее web-browser control может реализовывать собственную логику через переопределение объекта external. Естественно, чтобы иметь возможноть работать со своим приложением из скрипта, программа должна реализовывать dispatch-интерфейсы. При помощи ClassWizard, добавить поддержку автоматизации к своим объектам не составляет труда. Единственным замечанием здесь может служить лишь то, что объекты должны наследоваться от CCmdTarget. Чтобы передать указатель на свой объект самому объекту webbrowser, а заодно установить целую кучу дополнительных параметров, необходимо реализовать cлужебный интерфейс IDocHostUIHandler, который описан в mshtmhst.h. Этот интерфейс представляет собой некий call-back, или интерфейс обратной связи, к которому обращается webbrowser в следующих случаях:

  • Необходимо показать контекстное меню. Как раз здесь можно заменить стандартное меню Internet-explorer на свое собственное. Либо вообще сделать так, чтобы меню не показывлось.
  • Есть возможность подменить элементы пользовательского интерфейса браузера.
  • Нужно обработать нажатие горячей клавиши.
  • Нужно обработать URL, по которому совершается переход.
  • Нужно обработать события drag-and-drop.
  • Необходимо получить указатель на объект window.external.

После реализации этого call-back объекта, его можно "инсталлировать", используя метод интерфейса ICustomDoc SetUIHandler. Интерфейс IСustomDoc экспортируется обычно реализуется тем же объектом, что реализует IHTMLDocument2.

// код из OnNavigateComplete
CComQIPtr<ICustomDoc, &IID_ICustomDoc> m_pBrowserCustomDoc;
CComQIPtr<IHTMLDocument2,&IID_IHTMLDocument2> pADocument;
CDocHostUIHandler m_DocHostImpl;

m_DocHostImpl.AddRef();
m_DocHostImpl.m_pAppDisp = m_pApp->GetIDispatch(FALSE);
m_pBrowserCustomDoc = pADocument;
m_pBrowserCustomDoc->SetUIHandler((IDocHostUIHandler*)&m_DocHostImpl);

В данном коде фигурирует класс CDocHostUIHandler, который реализует все методы интерфейса IDocHostUIHandler (и конечно же AddRef, QueryInterface и Release от IUnknown). В базовом варианте, реализация этого объекта сводится лишь к созданию процедур-заглушек для каждого метода IDocHostUIHandler, возвращающих E_NOTIMPL. А если хочется, чтобы Internet Explorer не показывал своего конекстного меню, нужно возвращать из метода ShowContextMenu S_OK.

Если наш объект CDocHostUIHandler возвращает указатель в методе get_External, то этот указатель и используется как объект расширения и тогда где-нибудь внутри самой html странички можно будет написать такие строки:

<script language="JavaScript">
function ShowSettingsDialog()
{
  if (window.external.ShowSettings() == true)
 {
   document.body.bgcolor = window.external.BackColor;
 }
}
</script>

<body>
 <a href="javascript:ShowSettingsDialog()">Settings</a>
</body>

В приведенном примере, функция ShowSettings и свойство BackColor запрашиваются из недр нашего собственного приложения.

Где хранить свои HTML

В ресурсах! К счастью, Internet explorer умеет грузить из ресурсов, нужно только в качестве префикса URL написать res://<путь к модулю>/<название ресурса>. Я привожу реализацию этого метода, выдранную из исходного текста CHTMLView.

HINSTANCE hInstance = AfxGetResourceHandle();

CString strResourceURL;
BOOL bRetVal = TRUE;
LPTSTR lpszModule = new TCHAR[_MAX_PATH];

if (GetModuleFileName(hInstance, lpszModule, _MAX_PATH))
{
 // lpszResource - строкое название ресурса
 strResourceURL.Format(_T("res://%s/%s"), lpszModule, lpszResource);
 m_pBrowserApp->Navigate(strResourceURL, NULL, NULL, NULL, NULL);
}
else
 bRetVal = FALSE;

delete [] lpszModule;
return bRetVal;

HTML ресурсы можно вынести в отдельный подкаталог, например html. Тогда в файле описания ресурсов (например, myapp.rc) необходимо добавить строки следующего вида:

IDR_MAIN HTML DISCARDABLE "html\\main.html"
DEL.GIF HTML DISCARDABLE "html\\del.gif"
LEFTARR.GIF HTML DISCARDABLE "html\\leftarr.gif"
RIGHTARR.GIF HTML DISCARDABLE "html\\rightarr.gif"
TITLE.GIF HTML DISCARDABLE "html\\title.gif"
NEWMSG.GIF HTML DISCARDABLE "html\\newmsg.gif"

Реализация доступа к ресурсам в IE достаточно умна, чтобы автоматически найти все необходимые объекты в ресурсах, на которые ссылается страничка, поэтому достаточно знать лишь ресурс-имя основной странички.

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

Q| Насколько корректно будут работать методы контроля утечек памяти (в частности объект CMemoryState) в многопоточных приложениях?
У меня сложилось впечатление, что объект CMemoryState не делает различия в каком потоке вызывались операторы new с момента обращения к memState.Checkpoint() до обращения к memState.DumpAllObjectsSince().
Видимо "моментальные снимки" распределённой памяти в данном случае не информативны, ведь несколько потоков работают в одном адресном пространстве? - Николай Турпитко

|A Действительно, вне зависимости от потока, все распределения памяти попадают в один большой двусвязный список блоков памяти, который поддерживает отладочная версия CRT (если задан макрос _DEBUG). Что касается MFC-класса CMemoryState, он является просто тонкой обёрткой вокруг структуры _CrtMemState и функций для диагностики утечек памяти CRT. Поэтому он также не делает различий между потоками.

Хотя функции отладочной библиотеки очень полезны, они не отличаются гибкостью. Научить класс CMemoryState выдавать список блоков, выделенных текущим потоком, возможно, только используя недокументированные возможности CRT. Но кое-что в этом направлении можно сделать и легальными средствами.

При распределении памяти в отладочной версии программы каждому блоку назначается тип. По умолчанию блок получает тип _NORMAL_BLOCK. Существуют и другие типы: _CRT_BLOCK (блок, распределяемый для внутренних нужд CRT), _CLIENT_BLOCK (блок, к которому применяется пользовательская функция построения дампа), _FREE_BLOCK (блок, который уже освобождён с помощью free; такие блоки остаются в памяти, чтобы отладочная библиотека могла отследить ошибки, связанные с записью в уже освобождённый блок памяти) и _IGNORE_BLOCK (блок, который игнорируется при построении списка распределённых объектов). В стандартную библиотеку входит версия оператора new с четырьмя параметрами, которой можно передать тип распределяемого блока.

Соответственно, мы можем сохранить идентификатор потока, который нас интересует, в глобальной переменной, а затем передавать оператору new тип _NORMAL_BLOCK, если идентификатор текущего потока совпадает с сохранённым в переменной, и _IGNORE_BLOCK в противном случае. Чтобы облегчить эту задачу, можно написать небольшой модуль, который будет всем этим заниматься. Например:

//------------------------------
// threadmem.h
void DumpOnlyThisThread(DWORD id);
extern DWORD __DumpThread;

#ifdef _DEBUG
#define THREAD_DEBUG_NEW \
    new((__DumpThread == ::GetCurrentThreadId() ? \
    _IGNORE_BLOCK : _NORMAL_BLOCK), THIS_FILE, __LINE__)
#else
#define THREAD_DEBUG_NEW new
#endif /* _DEBUG */

//------------------------------
// threadmem.cpp
void DumpOnlyThisThread(DWORD id)
{
#ifdef _DEBUG
    InterlockedExchange((LONG *)&__DumpThread, id);
#endif /* _DEBUG */
}

DWORD __DumpThread;

Теперь функция потока, в котором мы хотим отслеживать утечки памяти, может выглядеть так:

#include "threadmem.h"
#define new THREAD_DEBUG_NEW

UINT ThreadFunc(LPVOID)
{
    DumpOnlyThisThread(::GetCurrentThreadId());

    CMemoryState st;
    st.Checkpoint();

    // Распределяем и освобождаем память в процессе работы...
    new int[100];
    new CPoint[200];
    ...

    st.DumpAllObjectsSince();

    return 0;
}

Объекты, распределённые во всех остальных потоках, не попадут в отчёт об утечках памяти.
- Александр Шаргин (rudankort@mail.ru)

/ / / В ПОИСКАХ ИСТИНЫ / / / / / / / / / / / / /

Q| Как создать окно ввода текста переключаясь в которое устанавливался бы заданный язык. например необходим ввод только русских слов в строке по которой ищется перевод на английский, а язык по умолчанию в виндовс английский. хотелесь бы при запуске программы когда пользователь ткнет мышкой в поле ввода чтобы он не переключал язык по умолчаню на русский. - Alexander Shinkevich

Ответить на вопрос

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Это все на сегодня. До встречи!

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


No.35

Выпуск No. 35 от 4 марта 2001 г.

Здравствуйте!

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

/ / / / СТАТЬЯ / / / / / / / / / / / / / / / / / / / / / /

MAPI. Добавь почту в свое приложение.

Автор: Михаил Плакунов
Источник: Софт Терра

Введение

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

Что есть MAPI?

В широком понимании MAPI (Messaging Application Programming Interface) - это целая архитектура, специфицирующая процессы взаимодействия отдельных приложений с различными почтовыми системами. Архитектура MAPI описывает так называемую подсистему MAPI, которая обеспечивает взаимодействие клиентских приложений с различными службами почтовой системы, такими как служба хранения информации, служба адресной книги, служба транспорта и т.д. С другой стороны MAPI - это прикладной интерфейс, который был создан для того, чтобы разработчики на С, С++, Visual Basic (а в последствии и Visual Basic Script) имели возможность добавлять в свои приложения функциональность для работы с электронной почтой. С точки зрения прикладной программы подсистема MAPI - это набор динамических библиотек, содержащих функции и объектно-ориентированные интерфейсы, благодаря которым взаимодействуют клиентские и серверные части почтовых приложений. О MAPI можно говорить много и долго (благо компания Microsoft постаралась сделать из MAPI очередного программного <монстра>), но наибольший интерес для разработчиков представляют так называемые клиентские прикладные программные интерфейсы, среди которых следует выделить в первую очередь Simple MAPI, MAPI и CDO.

Начнем с простого - Simple MAPI

Simple MAPI предоставляет в распоряжение разработчиков всего 12 простейших функций. Они позволяют выполнять такие действия, как <сформировать сообщение>, <указать адрес получателя>, <отправить>, <получить>. Причем все операции с сообщениями можно производить только в рамках одной папки, являющейся папкой для входящих сообщений текущего контейнера доставки (в почтовой системе MS Exchange Server это обычно или <Входящие> в русскоязычной версии). Разработчик не имеет доступа к полной структуре папок почтового сервера, то есть может контролировать сообщение лишь до тех пор, пока пользователь не переместит его из папки в какую-либо другую.

Другим недостатком Simple MAPI является то, что он позволяет работать только со стандартными полями сообщения, такими как <Тема>, <Отправитель>, <Получатель>, <Дата отправки>, <Текст сообщения>, <Класс сообщения>, а также с вложенными файлами.

При всей своей ограниченности Simple MAPI подкупает имено простотой в освоении и использовании. Тому, кто имеет даже небольшой опыт программирования на С или Visual Basic достаточно нескольких минут для, того чтобы научиться использовать этот интерфейс.

Следующий фрагмент кода на языке С позволит убедиться в этом (для простоты несущественные участки кода, такие как объявление переменных и обработка ошибок опущены).

MAPILogon(0,"My Profile", NULL, MAPI_NEW_SESSION, 0, &pSession);
MAPIResolveName(pSession, 0, "Bill Gates", 0, 0, &pRecipient);

ZeroMemory(&pMessage, sizeof(pMessage))
pMessage.lpszSubject = " Greeting";
pMessage.lpszNoteText = "Hello Bill!";
pMessage.nRecipCount = 1;
pMessage.lpRecips = Recipient;

MAPISendMail(pSession, 0, &pMessage, 0, 0);
MAPILogoff(pSession, 0, 0, 0);

В приведенном примере формируется сообщение с темой , содержащее текст и отправляется адресату, видимое имя которого в адресной книге . Сейчас мы разберем по шагам как это делается.

Первым делом клиентской программе необходимо начать сеанс работы с почтовой системой, для чего при помощи функции MAPILogon открывается сессия Simple MAPI. Затем из видимого имени ("Bill Gates") функция MAPIResolveName формирует структуру, содержащую точную и полную информацию об адресате (в частности его электронный адрес). Полученная информация об адресате наряду с темой и текстом формирует структуру, содержащую почтовое сообщение, готовое к отправке. Функция MAPISendMail отправляет сообщение по электронной почте. Наконец, функция MAPILogoff завершает сеанс работы с почтовой системой, закрывая сессию Simple MAPI.

Просто, не правда ли? Немного модифицировав программу, можно дать ей возможность отправлять сообщения, содержащие не только текст, но и вложенные файлы. Изучив еще пару-тройку функций Simple MAPI, можно с легкостью запрограммировать получение анализ и удаление сообщений, содержащихся в почтовом ящике пользователя.

Simple MAPI позволяет запрограммировать две основные функции электронной почты - отправку и прием сообщений. Зачастую это вся функциональность, необходимая приложению для работы с электронной почтой. Типичными примерами использования Simple MAPI являются приложения, производящие рассылку сообщений (возможно однотипных, по шаблону) множеству адресатов, а также приложения, время от времени сканирующие почтовый ящик пользователя и производящие анализ и обработку поступающей в него корреспонденции.

Программисты на С найдут определения всех функций, структур и констант Simple MAPI в файле MAPI.H, входящем в состав Microsoft Visual Studio. Его аналогом для Visual Basic является файл MAPI.BAS. Сами функции находятся в динамической библиотеке MAPI.DLL. Как правило Simple MAPI входит в состав клиентских почтовых программ, причем не только работающих в архитектуре <клиент-сервер> (MS Outlook, MS Exchange Client), но и обычных (MS Outlook Express, Eudora Pro, а в скором будущем и The Bat!).

MAPI 1.0 - для продвинутых

Simple MAPI на то и , что накладывает серьезные ограничения на разработчика как в плане функциональности, так и в плане производительности приложения. Полностью снять эти оковы позволяет гибкий и мощный программный интерфейс MAPI 1.0 (в прошлом - Extended MAPI по аналогии с Simple MAPI). MAPI 1.0 - это совокупность более ста функций и нескольких десятков COM-интерфейсов, предоставляющих программистам на С и С++ богатый инструментарий для создания приложений, работающих с электронной почтой. Simple MAPI можно назвать оберткой MAPI 1.0, которая скрывает множество деталей и нюансов взаимодействия приложений с почтовыми системами.

MAPI 1.0 предоставляет разработчику не только возможность реализации таких простых функций как отправка или прием почтовых сообщений, но и механизмы для более тесного взаимодействия с отдельными частями систем электронной почты - с адресной книгой, иерархической структурой папок на почтовом сервере, службой транспорта и т.д. Более того, с помощью MAPI 1.0 можно создавать даже части почтовых систем - программные шлюзы, различные службы обработки информации, которые являются частью MAPI-совместимых почтовых серверов. Не будет преувеличением сказать, что, используя MAPI 1.0 можно создать свою собственную клиентскую почтовую программу, аналогичную MS Oulook со всеми ее богатыми возможностями.

Вместе с тем создание приложений на базе MAPI 1.0 - более трудоемкий процесс, нежели программирование с использованием Simple MAPI. MAPI 1.0 требует от разработчика дополнительной квалификации, в частности знания технологии COM. Перепишем уже имеющийся пример с использованием MAPI 1.0. Для наглядности каждый блок кода сопоставлен с соответствующим фрагментом из предыдущего примера.

// Begin MAPILogon(:);
MAPILogonEx(0, "My Profile", NULL, MAPI_NEW_SESSION, &lpSession);
// End MAPILogon(:);

lpSession->GetMsgStoresTable(0, &StoresTable);
HrQueryAllRows(StoresTable, (LPSPropTagArray)&tagDefaultStore, NULL, NULL, 0, &lpRow);
for(i = 0; i < lpRow -> cRows; i++)
{
  if(lpRow->aRow[i].lpProps[0].Value.b == TRUE)
  break;
}

lpSession->OpenMsgStore(0, lpRow -> aRow[i].lpProps[1].Value.bin.cb,
                                            (LPENTRYID)lpRow -> aRow[i].lpProps[1].Value.bin.lpb, NULL,
                                            MDB_WRITE, &lpMDB);

lpMDB->OpenEntry(lpPropValue -> Value.bin.cb, (LPENTRYID)lpPropValue -> Value.bin.lpb,
                                  NULL, MAPI_MODIFY, &ulObjType, (LPUNKNOWN *)&lpFolder);

lpFolder->CreateMessage(NULL, 0, &lpMsg);
SInitPropValue MsgProps[] =
{
  {PR_DELETE_AFTER_SUBMIT, 0, TRUE},
  {PR_MESSAGE_CLASS, 0, (ULONG)"IPM.NOTE "},
  {PR_SUBJECT, 0, (ULONG)"Greeting"},
  {PR_BODY, 0, (ULONG)" Hello Bill!"}
};
lpMsg->SetProps(4, (LPSPropValue)&MsgProps, NULL);

// Begin MAPIResolveName(:);
lpSession->OpenAddressBook(0, NULL, AB_NO_DIALOG, &lpAdrBook);
MAPIAllocateBuffer(CbNewADRLIST(1), (LPVOID*)&lpAdrList);
MAPIAllocateBuffer(2*sizeof(SPropValue), (LPVOID*)&(lpAdrList -> aEntries -> rgPropVals));

ZeroMemory(lpAdrList -> aEntries -> rgPropVals, 2*sizeof(SPropValue));
lpAdrList->cEntries = 1;
lpAdrList->aEntries[0].ulReserved1 = 0;
lpAdrList->aEntries[0].cValues = 2;
lpAdrList->aEntries[0].rgPropVals[0].ulPropTag  = PR_DISPLAY_NAME;
lpAdrList->aEntries[0].rgPropVals[0].Value.lpszA = "Bill Gates";
lpAdrList->aEntries[0].rgPropVals[1].ulPropTag  = PR_RECIPIENT_TYPE;
lpAdrList->aEntries[0].rgPropVals[1].Value.l    = MAPI_TO;

lpAdrBook->ResolveName(0, 0, NULL, lpAdrList);
lpMsg->ModifyRecipients(MODRECIP_ADD, lpAdrList);
// End MAPIResolveName(:);

// Begin MAPISendMail(:);
lpMsg->SubmitMessage(0);
// End MAPISendMail(:);

// Begin MAPILogoff (:);
lpSession->Logoff(0, 0, 0);
// End MAPILogoff (:);

Как видно из этого примера большинство операций, являющихся примитивными для Simple MAPI, в MAPI 1.0 состоят из последовательностей вызовов тех или иных методов различных интерфейсов. Так, для того, чтобы создать сообщение в MAPI 1.0 требуется получить доступ и открыть контейнер с сообщениями, отыскать в нем папку для исходящих сообщений и только потом собственно создать в ней само сообщение и начинить его всей необходимой информацией. В Simple MAPI все эти промежуточные шаги скрыты от разработчика. С другой стороны MAPI 1.0 позволяет создавать сообщения в любой папке (да и не только сообщения, а объекты календаря, задачи и т.д.). Таким образом, взамен простоте использования появляются новые возможности.

Интерфейс MAPI 1.0, в отличие от Simple MAPI можно использовать при создании служб Windows NT. Это очень полезное свойство позволяет создавать различного рода почтовые мониторы. Типичная задача почтового монитора может заключаться в сканировании почтового ящика пользователя на предмет поступающей в него корреспонденции, ее разбор, анализ и последующие действия по результатам этого анализа.

Отдельного обсуждения заслуживает такая возможность MAPI 1.0 как создание всевозможных расширений (extensions) к клиентским программам почтовой системы MS Exchange Server (MS Outlook или MS Exchange Client). Расширения позволяют автоматизировать различные функции обработки сообщений, не реализованные в базовом наборе функций клиентской программы. В частности, механизм расширений позволяет создавать модули для обработки входящих сообщений, так называемые правила (rules), добавлять к клиентской программе дополнительные команды и пункты меню, а также обработчики событий, изменяющие поведение системы при определенных событиях и многое другое.

CDO - разумный компромисс

Интерфейс CDO (Collaboration Data Objects), ранее известный как OLE Messaging и Active Messaging представляет собой библиотеку, обеспечивающую доступ приложений к несколько ограниченному набору функция MAPI 1.0 через вызовы Automation. Функции работы с сообщениями могут быть встроены в приложения, созданные с помощью любого средства разработки, являющегося контроллером Automation. К таковым относятся C/C++, Visual Basic, Visual Basic for Applications, VBScript, Javascript. Использование CDO существенно упрощает разработку приложений, работающих с электронной почтой, вместе с тем оставляя разработчику широкие возможности MAPI. Наибольшее применение CDO находит в скриптовых языках. Так, например, в комбинации с APS использование CDO позволяет достаточно легко создать почтового Web-клиента.

Сухой остаток

Итак, мы кратко рассмотрели 3 программных интерфейса, позволяющих встраивать в приложения на платформе Windows функциональность для работы с электронной почтой. У каждого из них есть свои преимущества и недостатки. Ничего универсального не существует - окончательный выбор того или иного средства зависит от конкретной задачи и является прерогативой разработчика. Более подробную информацию на эту тему можно получить на http://msdn.microsoft.com/library/psdk/mapi/.

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

Q| Как создать окно ввода текста переключаясь в которое устанавливался бы заданный язык. например необходим ввод только русских слов в строке по которой ищется перевод на английский, а язык по умолчанию в виндовс английский. хотелесь бы при запуске программы когда пользователь ткнет мышкой в поле ввода чтобы он не переключал язык по умолчаню на русский. - Alexander Shinkevich

|A1 Для переключения раскладок необходимо вызвать функцию LoadKeyboardLayout

Ниже приводится пример ее использования :

1) Добавить в проект с помощью Class Wizard'а новый класс CMyEdit на основе CEdit.
2) Добавить в класс переменную, хранящую предыдущую установленную раскладку клавиатуры :

TCHAR m_PreviousLayout[KL_NAMELENGTH] ;

3) Добавить обработчики WM_SETFOCUS и WM_KILLFOCUS :

void CMyEdit::OnSetFocus(CWnd* pOldWnd)
{
  CEdit::OnSetFocus( pOldWnd ) ;

  // запоминаем предыдущую раскладку клавиатуры
  ::GetKeyboardLayoutName( m_PreviousLayout ) ;
  // устанавливаем новую раскладку для языка "Русский"
  ::LoadKeyboardLayout( _T( "00000419" ), KLF_ACTIVATE ) ;
}

void CMyEdit::OnKillFocus(CWnd* pNewWnd)
{
  CEdit::OnKillFocus(pNewWnd);

  // восстанавливаем предыдущую раскладку клавиатуры
  ::LoadKeyboardLayout( m_PreviousLayout, KLF_ACTIVATE ) ;
}

4) Использовать CMyEdit вместо CEdit (на примере диалога) :

class CMyDlg : public CDialog
{
  // ...
  CMyEdit m_Edit;
  // ...
} ;

void CMyDlg::DoDataExchange(CDataExchange* pDX)
{
  CDialog::DoDataExchange(pDX);
  //{{AFX_DATA_MAP(CTestKeyboardDlg)
  DDX_Control( pDX, IDC_EDIT, m_Edit ) ;
  //}}AFX_DATA_MAP
}
- Алексей Гончаров

|A2 [...] Также можно активизировать т.н. keyboard layout (раскладку клавиатуры) с помощью функции ActivateKeyboardLayout, активизирующей раскладку, загруженную предварительно с помощью указанной выше функции LoadKeyboardLayout. Хотя LoadKeyboardLayout сама может активизировать раскладку (при использовании флага KLF_ACTIVATE ), но при частой смене языка оптимальнее использовать ActivateKeyboardLayout. Т.е. в начале загрузить раскладку с помощью LoadKeyboardLayout, а многократно переключать язык ввода функцией ActivateKeyboardLayout. - Igor Sukharev

/ / / В ПОИСКАХ ИСТИНЫ / / / / / / / / / / / / /

Q| Можно ли из моей программы управлять окном которое создано другим приложением (закрывать, сворачивать, нажимать в нем кнопки и т.д.), если да то как? - Alhim

Ответить на вопрос

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

А на сегодня это все. До встречи через неделю!

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


No.36

Выпуск No. 36 от 11 марта 2001 г.

Здравствуйте!

Ну вот, готова моя первая статья из серии, посвященной межпроцессному обмену данными. Эта статья является обзорной, ее цель - познакомить вас со всем многообразием механизмов Windows для обмена данными между процессами. В последующих статьях я остановлюсь на основных методах поподробнее. Хотя, к сожалению не могу обещать что эти статьи появятся скоро. Вы, однако, можете повлиять на мой выбор тем для подробного рассмотрения, если сообщите мне заранее, какая именно из них вас особенно интересует.

/ / / / СТАТЬЯ / / / / / / / / / / / / / / / / / / / / / /

IPC: основы межпроцессного взаимодействия
 Обзор технологий

Введение

Любая операционная система была бы весьма ущербна, если бы замыкала выполняющееся приложение в собственном темном мирке без окон и дверей, без какой-либо возможности сообщить другим программам какую-либо информацию. Если посмотреть внимательно, можно заметить, что далеко не все приложения являются самодостаточными. Очень многим, если не большей части, требуется информация от других приложений, либо они должны эту информацию сообщать. Именно поэтому в операционную систему встраивается множество механизмов, которые обеспечивают т.н. Interproccess Communication (IPC) - то есть межпроцессное взаимодействие.

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

Рассмотрим подробнее несколько ключевых примеров, демонстрирующих важность IPC. Вам, возможно это покажется неправдоподобным, но зачатки IPC существовали еще в MS-DOS - и это несмотря на то, что MS-DOS при всем желании трудно назвать многозадачной средой. В самом деле, когда вы в командной строке вводили подобную инструкцию :

C:\>DIR|MORE

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

Примеры использования IPC охватывают гораздо большее количество программ и приложений, чем вы скорее всего думаете. Когда вы выходите в интернет, ваш браузер - одна программа (процесс) - взаимодействует с web-сервером - другой программой (процессом). Эти программы выполняются на разных компьютерах; браузер на вашем, сервер - где-то еще. И вас не волнует, какая ОС установлена на сервере и какая там платформа.
Или, например, вы работаете с удаленной базой данных. Ваше клиентское приложение - это один процесс, на сервере базы данных запущен другой процесс. Процесс на сервере выполняет запросы к БД, поступающие от вашего процесса.

ПРИМЕЧАНИЕ
Вообще, для сетевых форм IPC (но не обязательно только для них) очень часто используется концепция "клиент-сервер". Как вы понимаете, "клиент" - это приложение, которому требуются данные, "сервер" - приложение, предоставляющее данные.

А если брать только взаимодействие программ, выполняющихся на одном компьютере, самым банальным примером будет следующий: текст из вашего текcтового редактора передается в электронную таблицу или программу для верстки. Да-да, наш старый знакомый буфер обмена - это тоже один из механизмов IPC!
И еще можно было бы привести очень много примеров.

Средств, обеспечивающих взаимодействие между процессами, создано достаточно много. Огромное их количество реализовано в Windows 9x, еще больше - в Windows NT/2000. Теперь нужно приличное количество времени, чтобы хотя бы познакомиться со всеми! Замечу, что нет, и наверное в принципе не может быть универсального способа обмена данными, который годился бы на все случаи жизни - все равно в некоторых случаях использование другого способа будет предпочтительнее. Но я надеюсь, что после прочтения этой статьи вы сможете достаточно уверенно ориентироваться в мире IPC и обоснованно выбирать тот или иной метод.

Тот факт, что механизмы IPC работают на уровне операционной системы, положительно сказывается на скорости и надежности программ и программных комплексов, построенных с их использованием. Эффективность приложений соответственно возрастает.

Вообще, правильнее было бы называть эти механизмы "Interthread Communication" - межпотоковое взаимодействие. Если вы помните, выполняются именно потоки, они же и обмениваются данными. Однако, смысл для отдельных механизмов взаимодействия появляется только в том случае, если эти потоки принадлежат разным процессам. Ведь потоки, выполняющиеся в рамках одного процесса, вовсе не нуждаются в дополнительных средствах для общения между собой. Так как они разделяют одно адресное пространство, обмен данными могут обеспечить обычные переменные. Таким образом, IPC становится необходим в том случае, если поток одного процесса должен передать данные потоку другого процесса.

Теперь давайте рассмотрим основные виды IPC и случаи, в которых они используются.

Буфер обмена (clipboard)
Это одна из самых примитивных и хорошо известных форм IPC. Он появился еще в самых ранних версиях Windows. Основная его задача - обеспечивать обмен данными между программами по желанию и под контролем пользователя. Впрочем, вы наверняка сами неплохо знаете, как используется буфер обмена... ;-) Не рекомендуется использовать его для внутренних нужд приложения, и не стоит помещать туда то, что не предназначено для прямого просмотра пользователем.
Сообщение WM_COPYDATA
Стандартное сообщение для передачи участка памяти другому процессу. Работает однонаправленно, принимающий процесс должен расценивать полученные данные как read only. Посылать это сообщение необходимо только с помощью SendMessage, которая (напомню) в отличие от PostMessage ждет завершения операции. Таким образом, посылающий поток "подвисает" на время передачи данных. Вы сами должны решить, насколько это приемлемо для вас. Это не имеет значения для небольших кусков данных, но для больших объемов данных или для real-time приложений этот способ вряд ли подходит.
Разделяемая память (shared memory)
Этот способ взаимодействия реализуется не совсем напрямую, а через технологию File Mapping - отображения файлов на оперативную память. Вкраце, этот механизм позволяет осуществлять доступ к файлу таким образом, как будто это обыкновенный массив, хранящийся в памяти (не загружая файл в память явно). "Побочным эффектом" этой технологии является возможность работать с таким отображенным файлом сразу нескольким процессам. Таким образом, можно создать объект file mapping, но не ассоциировать его с каким-то конкретным файлом. Получаемая область памяти как раз и будет общей между процессами. Работая с этой памятью, потоки обязательно должны согласовывать свои действия с помощью объектов синхронизации.
Библиотеки динамической компоновки (DLL)
Библиотеки динамической компоновки также имеют способность обеспечивать обмен данными между процессами. Когда в рамках DLL объявляется переменная, ее можно сделать разделяемой (shared). Все процессы, обращающиеся к библиотеке, для таких переменных будут использовать одно и то же место в физической памяти. (Здесь также важно не забыть о синхронизации.)
Протокол динамического обмена данными (Dynamic Data Exchange, DDE)
Этот протокол выполняет все основные функции для обмена данными между приложениями. Он очень широко использовался до тех пор, пока для этих целей не стали применять OLE (впоследствии ActiveX). На данный момент DDE используется достаточно редко, в основном для обратной совместимости.

Больше всего этот протокол подходит для задач, не требующих продолжительного взаимодействия с пользователем. Пользователю в некоторых случаях нужно только установить соединение между программами, а обмен данными происходит без его участия. Замечу, что все это в равной степени относится и к технологии OLE/ActiveX.

OLE/ActiveX
Это действительно универсальная технология, и одно из многих ее применений - межпроцессный обмен данными. Хотя cтоит думаю отметить, что OLE как раз для этой цели и создавалась (на смену DDE), и только потом была расширена настолько, что пришлось поменять название ;-). Специально для обмена данными существует интерфейс IDataObject. А для обмена данными по сети используется DCOM, которую под некоторым углом можно рассматривать как объединение ActiveX и RPC.
Каналы (pipes)
Каналы - это очень мощная технология обмена данными. Наверное, именно поэтому в полной мере они поддерживаются только в Windows NT/2000. В общем случае канал можно представить в виде трубы, соединяющей два процесса. Что попадает в трубу на одном конце, мгновенно появляется на другом. Чаще всего каналы используются для передачи непрерывного потока данных.

Каналы делятся на анонимные (anonymous pipes) и именованные (named pipes).
Анонимные каналы используются достаточно редко, они просто передают поток вывода одного процесса на поток ввода другого.
Именованные каналы передают произвольные данные и могут работать через сеть. (Именованные каналы поддерживаются только в WinNT/2000.)

Сокеты (sockets)
Это очень важная технология, т.к. именно она отвечает за обмен данными в Интернет. Сокеты также часто используются в крупных ЛВС. Взаимодействие происходит через т.н. разъемы-"сокеты", которые представляют собой абстракцию конечных точек коммуникационной линии, соединяющей два приложения. С этими объектами программа и должна работать, например, ждать соединения, посылать данные и т.д. В Windows входит достаточно мощный API для работы с сокетами.
Почтовые слоты (mailslots)
Почтовые слоты - это механизм однонаправленного IPC. Если приложению известно имя слота, оно может помещать туда сообщения, а приложение-хозяин этого слота (приемник) может их оттуда извлекать и соответствующим образом обрабатывать. Основное преимущество этого способа - возможность передавать сообщения по локальной сети сразу нескольким компьютерам за одну операцию. Для этого приложения-приемники создают почтовые слоты с одним и тем же именем. Когда в дальнейшем какое-либо приложение помещает сообщение в этот слот, приложения-приемники получают его одновременно.
Объекты синхронизации
Как ни странно, объекты синхронизации тоже можно отнести к механизмам IPC. Конечно, объем передаваемых данных в данном случае очень невелик ;) Но именно эти объекты следует использовать, если одному процессу нужно передать другому что-то вроде "я закончил работу" или "я начинаю работать с общей памятью".
Microsoft Message Queue (MSMQ)

Этот протокол действительно оправдывает свое название - он обеспечивает посылку сообщений между приложениями с помощью очереди сообщений. Основное его отличие от стандартной очереди сообщений Windows в том, что он может работать с удаленными процессами и даже с процессами, которые на данный момент недоступны (например, не запущены). Доставка сообщения по адресу гарантируется. Оно ставится в специальную очередь сообщений и находится там до тех пор, пока не появляется возможность его доставить.

Удаленный вызов процедур (Remote Procedure Call, RPC)
Строго говоря, это не совсем технология IPC, а скорее способ значительно расширить возможности других механизмов IPC. С помощью этой технологии общение через сеть становится совешенно прозрачным как для сервера, так и для клиента. Им обоим начинает казаться, что их "собеседник" расположен локально по отношению к ним .
Резюме
Конечно, я перечислил далеко не все способы обмена данными. Если бы это было так, то это было бы не так интересно ;-) За рамками данной статьи остались такие вещи, как глобальная таблица атомов, хуки и некоторые другие технологии, которые с некоторой натяжкой можно признать механизмами IPC. Но главное, как я считаю, сделано: теперь вы знаете, что это за непонятные аббревиатуры и как из всего многообразия методов IPC выбрать наиболее подходящий.

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

Q| Можно ли из моей программы управлять окном которое создано другим приложением (закрывать, сворачивать, нажимать в нем кнопки и т.д.), если да то как? - Alhim

|A Выполнение этой задачи распадается на два этапа.

Сначала нужно каким-то образом определить хэндл окна, которым мы собираемся манипулировать. Основным инструментом здесь являются функции FindWindow(Ex), которые ищут окно по заданному классу и/или заголовку. В определении и того, и другого сильно помогает программа Spy++. Рассмотрим пример поиска HWND стандартной кнопки "Пуск". Сначала используем Spy++, чтобы определить классы панели задач и самой кнопки; оказывается, их имена "Shell_TrayWnd" и "Button" соответственно. Затем используем FindWindow(Ex).

 HWND hWnd;
 hWnd = FindWindow("Shell_TrayWnd", NULL);
 hWnd = FindWindowEx(hWnd, NULL, "Button", NULL);

 if (IsWindow(hWnd))
 {
  // Кнопка найдена, работаем с ней
 }
Ещё один набор функций, которые могут помочь в поиске хэндла чужого окна - это EnumChildWindows, EnumThreadWindows и EnumWindows, перечисляющие все окна, принадлежащие заданному окну, все окна заданного потока и все окна в системе соответственно. За описанием этих функций следует обратиться к документации. Кроме перечисленного можно упомянуть случай, когда приложение специально проектируется для взаимодействие с другим посредством обмена сообщениями. Например, одно приложение запускает другое, а затем обменивается с ним данными посредством WM_COPYDATA. В этом случае вполне уместно передать хэндл окна (это 4-хбайтовое целое) как параметр командной строки. После того, как хэндл окна определён, можно переходить ко второму этапу - управлению окном. Многие функции позволяют работать с окном, вне зависимости от того, какому процессу оно принадлежит. Характерные примеры таких функций - ShowWindow и SetForegroundWindow. Для примера рассмотрим, как спрятать кнопку "Пуск", получать хэндл которой мы уже научились.

 HWND hWnd;
 hWnd = FindWindow("Shell_TrayWnd", NULL);
 hWnd = FindWindowEx(hWnd, NULL, "Button", NULL);

 if (IsWindow(hWnd))
 {
  ShowWindow(hWnd, SW_HIDE);
  Sleep(5000);
  ShowWindow(hWnd, SW_SHOW); // Показываем обратно
 }

Кроме использования подобных функций, можно посылать окну сообщения. Например, послав кнопке BM_CLICK (с помощью PostMessage), мы как бы нажимаем на неё.

Проблемы возникают с функциями, которые позволяют работать только с окнами, созданными в том же потоке, в котором вызывается функция. В качестве примера приведу функцию DestroyWindow. Похожая проблема возникает, когда нужно "сабкласить" окно чужого процесса. В этих случаях необходимо внедрить свой код в чужой процесс и выполнить его в чужом потоке; удобнее всего сделать эт о, если код оформлен в виде DLL.

Существует несколько способов внедрить DLL в чужой процесс. Я покажу один из них; он достаточно прост и работает на всех Win32-платформах (Windows 9x, Windows NT), но в некоторых случаях недостаточно точен. Этот способ подразумевает установку хука на поток, создавший интересующее нас окно. При этом DLL, содержащая функцию хука, загружается системой в адресное пространство чужого процесса. Это как раз то, что нам нужно. А функцию хука вполне можно оставить пустой.

Рассмотрим пример DLL, которая уничтожает окно чужого процесса. (Такие вещи нужно делать, только полностью отдавая себе отчёт о возможных последствиях. Процесс, оставленный без окна, имеет хорошие шансы "рухнуть").

// _KillDll.cpp : Defines the entry point for the DLL application.
//

#include <windows.h>

// Создаём переменную в разделяемом сегменте,
// чтобы передать HWND из программы в DLL в чужом процессе.
#pragma comment(linker, "/SECTION:SHARED,RWS")
#pragma data_seg("SHARED")
__declspec(allocate("SHARED")) HWND hWndToKill = NULL;
#pragma data_seg()

BOOL APIENTRY DllMain( HANDLE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved )
{
 if (ul_reason_for_call == DLL_PROCESS_ATTACH &&
  IsWindow(hWndToKill) &&
  GetWindowThreadProcessId(hWndToKill, NULL) == GetCurrentThreadId())
 {
  // Если окно существует и принадлежит текущему потоку, убиваем его.
  HANDLE hEvent = OpenEvent(NULL, FALSE,
"{1F6C5480-155E-11d5-93A8-444553540000}");
  DestroyWindow(hWndToKill);
  SetEvent(hEvent);
  CloseHandle(hEvent);
 }

  return TRUE;
}

// Пустая функция хука.
LRESULT CALLBACK GetMsgProc(int code, WPARAM wParam, LPARAM lParam)
{
 return 1;
}

extern "C" __declspec(dllexport) void KillWndNow(HWND hWnd)
{
 if (!IsWindow(hWnd))
  return;

 hWndToKill = hWnd;

 HANDLE hEvent = CreateEvent(NULL, TRUE, FALSE,
"{1F6C5480-155E-11d5-93A8-444553540000}");

 DWORD dwThread = GetWindowThreadProcessId(hWnd, NULL);
 HHOOK hHook = SetWindowsHookEx(
  WH_GETMESSAGE,
  GetMsgProc,
  GetModuleHandle("_KillDll.dll"),
  dwThread);
 PostThreadMessage(dwThread, WM_NULL, 0, 0);

 WaitForSingleObject(hEvent, INFINITE);
 CloseHandle(hEvent);

 UnhookWindowsHookEx(hHook);
}

Чтобы использовать эту DLL, просто подключите её к программе (проще всего сделать это неявным методом), а затем выполните код:

extern "C" void KillWndNow(HWND hWnd);
...
HWND hWnd;
// Ищем окно
KillWndNow(hWnd);

Хотя код DLL сам по себе и небольшой, в нём есть несколько тонкостей, на которые я хотел бы обратить ваше внимание. Во-первых, я поместил переменную hWndToKill в разделяемый сегмент. Поскольку функция DestroyWindow вызывается в потоке чужого процесса, необходимо предусмотреть некоторый способ передачи хэндла окна через границы процессов. Разделяемая переменная - наиболее простое средство достичь цели. Во-вторых, DLL, содержащая функцию хука, не будет спроектирована на адресное пространство чужого процесса, пока функция хука реально не понадобится. В нашем случае хук имеет тип WH_GETMESSAGE, а значит DLL не загрузится, пока поток не получит какое-либо сообщение. Поэтому я посылаю ему сообщение WM_NULL (с кодом 0), чтобы вынудить ОС загрузить DLL. В-третьих, обратите внимание на применение события для синхронизации потоков в нашем и целевом процессах. Разумеется, для этой цели можно использовать и любой другой механизм синхронизации потоков. - Александр Шаргин (rudankort@mail.ru)

/ / / В ПОИСКАХ ИСТИНЫ / / / / / / / / / / / / /

Q| Хотелось бы побольше узнать о предварительном просмотре. В русской программе он смотрится инородным телом на своем иностранном языке. Можно ли его как-то настраивать под себя?
В этой же связи: не могу решить проблему.
В программе 3 меню и, соответственно, 3 панели инструментов, которые создал в Create. Переключая меню, вызываю ShowControlBar - прячу ненужные панели и показываю необходимую. Но после вызова PRINT PREVIEW, в окне появляются сразу все 3 панели инструментов.
Попутно: что означает AFX_IDS_PREVIEW_CLOSE в String Table? - Serg Petukhov

Ответить на вопрос

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Успехов!

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


No.37

Выпуск No. 37 от 18 марта 2001 г.

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

Сегодня нас ждет новая статья нашего постоянного автора Александра Шаргина, на этот раз посвященная стандартной библиотеке шаблонов C++.

/ / / / СТАТЬЯ / / / / / / / / / / / / / / / / / / / / / /

Введение в STL
Часть 1

Автор: Александр Шаргин
rudankort@mail.ru

Стандартная библиотека шаблонов (Standard Template Library, STL) входит в стандартную библиотеку языка C++. В неё включены реализации наиболее часто используемых контейнеров и алгоритмов, что избавляет программистов от рутинного переписывания их снова и снова. При разработке контейнеров и применяемых к ним алгоритмов (таких как удаление одинаковых элементов, сортировка, поиск и т. д.) часто приходится приносить в жертву либо универсальность, либо быстродействие. Однако разработчики STL поставили перед собой сверхзадачу - сделать библиотеку одновременно эффективной и универсальной. Надо признать, что им удалось достичь цели, хотя для этого и пришлось использовать наиболее продвинутые возможности языка C++, такие как шаблоны и перегрузка операторов.

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

Стандарт языка C++ не регламетнирует реализацию контейнеров и алгоритмов STL. Поэтому с каждым компилятором поставляется своя реализация этой библиотеки. В последующем изложении я буду опираться на реализацию, поставляемую фирмой Microsoft вместе с компилятором Visual C++ 6.0. Тем не менее, большая часть сказанного будет справедлива и для других реализаций STL.

Основные концепции STL

Краеугольными камнями STL являются понятия контейнера (container), алгоритма (algorithm) и итератора (iterator).

  • Контейнер - это хранилище объектов (как встроенных, так и определённых пользователем типов). Простейшие виды контейнеров (статические и динамические массивы) встроены непосредственно в язык C++. Кроме того, стандартная библиотека включает в себя реализации таких контейнеров, как вектор (vector), список (list), очередь (deque), ассоциативный массив (map), множество (set), и некоторых других.

  • Алгоритм - это функция для манипулирования объектами, содержащимися в контейнере. Типичные примеры алгоритмов - сортировка и поиск. В STL реализовано порядка 60 алгоритмов, которые можно применять к различным контейнерам, в том числе к массивам, встроенным в язык C++.

  • Итератор - это абстракция указателя, то есть объект, который может ссылаться на другие объекты, содержащиеся в контейнере. Основные функции итератора - обеспечение доступа к объекту, на который он ссылается (разыменование), и переход от одного элемента контейнера к другому (итерация, отсюда и название итератора). Для встроенных контейнеров в качестве итераторов используются обычные указатели. В случае с более сложными контейнерами итераторы реализуются в виде классов с набором перегруженных операторов.

Рассмотрим эти концепции более подробно.

Итераторы

Итераторы используются для доступа к элементам контейнера так же, как указатели - для доступа к элементам обычного массива. Как мы знаем, в языке C++ над указателями можно выполнять следующий набор операций: разыменование, инкремент/декремент, сложение/вычитание и сравнение. Соответственно, любой итератор реализует все эти операции или некоторое их подмножество. Кроме того, некоторые итераторы позволяют работать с объектами в режиме "только чтение" или "только запись", тогда как другие предоставляют доступ и на чтение, и на запись. В зависимости от набора поддерживаемых операций различают 5 типов итераторов, которые приведены в следующей таблице.

Тип итератора | Доступ | Разыменование | Итерация | Сравнение
Итератор вывода (output iterator) | Только запись | * | ++ |
Итератор ввода (input iterator) | Только чтение | *, -> | ++ | ==, !=
Прямой итератор (Forward iterator) | Чтение и запись | *, -> | ++ | ==, !=
Двунаправленный итератор (bidirectional iterator) | Чтение и запись | *, -> | ++, -- | ==, !=
Итератор с произвольным доступом (random-access iterator) | Чтение и запись | *, ->, [] | ++, --, +, -, +=, -= | ==, !=, <, <=, >, >=

Итератор с произвольным доступом реализует полный набор операций, применимых к обычным указателям.

Контейнеры

Как мы уже знаем, контейнер предназначен для хранения объектов. Хотя внутреннее устройство контейнеров очень сильно различается, каждый контейнер обязан предоставить строго определённый интерфейс, через который с ним будут взаимодействовать алгоритмы. Этот интерфейс обеспечивают итераторы. Каждый контейнер обязан иметь соответствующий ему итератор (и только итератор). Важно подчеркнуть, что никакие дополнительные функции-члены для взаимодействия алгоритмов и контейнеров не используются. Это сделано потому, что стандартные алгоритмы должны работать в том числе со встроенными контейнерами языка C++, у которых есть итераторы (указатели), но нет ничего, кроме них. Таким образом при написании собственного контейнера реализация итератора - необходимый минимум.

Каждый контейнер реализует определённый тип итераторов. При этом выбирается наиболее функциональный тип итератора, который может быть эффективно реализован для данного контейнера. "Эффективно" означает, что скорость выполнения операций над итератором не должна зависеть от количества элементов в контейнере. Например, для вектора реализуется итератор с произвольным доступом, а для списка - двунаправленный. Поскольку скорость выполнения операции [] для списка линейно зависит от его длины, итератор с произвольным доступом для списка не реализуется.

Вне зависимости от фактической организации контейнера (вектор, список, дерево) хранящиеся в нём элементы можно рассматривать как последовательность. Итератор первого элемента в этой последовательности вгозвращает функция begin(), а итератор элемента, следующего за последним - функция end(). Это очень важно, так как все алгоритмы в STL работают именно с последовательностями, заданными итераторами начала и конца.

Кроме обычных итераторов в STL существуют обратные итераторы (reverse iterator). Обратный итератор отличается тем, что просматривает последовательность элементов в контейнере в обратном порядке. Другими словами, операции + и - у него меняются местами. Это позволяет применять алгоритмы как к прямой, так и к обратной последовательности элементов. Например, с помощью функции find можно искать элементы как "с начала", так и "с конца" контейнера.

Каждый класс контейнера, реализованный в STL, описывает набор типов, связанных с контейнером. При написании собственных контейнеров следует придерживаться этой же практики. Вот список наиболее важных типов:

  • value_type - тип элемента
  • size_type - тип для хранения числа элементов (обычно size_t)
  • iterator - итератор для элементов контейнера
  • key_type - тип ключа (в ассоциативном контейнере)

Помимо типов можно выделить набор функций, которые реализует почти каждый контейнер в STL. Они не требуются для взаимодействия с алгоритмами, но их реализация улучшает взаимозаменяемость контейнеров в прграмме. Если, к примеру, какой-то контейнер реализует набор характерных для списка функций, то его можно будет вставить в программу вместо списка, изменив в ней всего одну строчку. Список основных функций приведён в таблице.

Функция | Описание
begin, end | Возвращают итераторы начала и конца прямой последовательности.
rbegin, rend | Возвращают итераторы начала и конца обратной последовательности.
front, back | Возвращают ссылки на первый и последний элемент, хранящийся в контейнере.
push_back, pop_back | Позволяют добавить или удалить последний элемент в последовательности.
push_front, pop_front | Позволяют добавить или удалить первый элемент в последовательности.
size | Возвращает количество элементов в контейнере.
empty | Проверяет, есть ли в контейнере элементы.
clear | Удаляет из контейнера все элементы.
insert, erase | Позволяют вставить или удалить элемент(ы) в середине последовательности.
Алгоритмы

Мы уже установили две важные вещи. Во-первых, алгоритмы предназначены для манипулирования элементами контейнера. Во-вторых, любой алгоритм рассматривает содержимое контейнера как последовательность, задаваемую итераторами первого и следующего за последним элементов. Итераторы обеспечивают интерфейс между контейнерами и алгоритмами, благодаря чему и достигается гибкость и универсальность библиотеки STL.

Каждый алгоритм использует итераторы определённого типа. Например, алгоритм простого поиска (find) просматривает элементы подряд, пока нужный не будет найден. Для такой процедуры вполне достаточно итератора ввода. С другой стороны, алгоритм более быстрого двоичного поиска (binary_search) должен иметь возможность переходить к любому элементу последовательности, и поэтому требует итератора с произвольным доступом. Вполне естественно, что вместо менее функционального итератора можно передать алгоритму более функциональный, но не наоборот.

Все стандартные алгоритмы описаны в файле algorithm, в пространстве имён std.

Вспомогательные компоненты STL

Помимо уже рассмотренных элементов в STL есть ряд второстепенных понятий, с которыми следует познакомиться.

Аллокаторы

Аллокатор (allocator) - это объект, отвечающий за распределение памяти для элементов контейнера. С каждым стандартным контейнером связывается аллокатор (его тип передаётся как один из параметров шаблона). Если какому-то алгоритму требуется распределять память для элементов, он обязан делать это через аллокатор. В этом случае можно быть уверенным, что распределённые объекты будут уничтожены правильно.

В состав STL входит стандартный класс allocator (описан в файле xmemory). Именно его по умолчанию используют все контейнеры, реализованные в STL. Однако никто не мешает вам реализовать собственный. Необходимость в этом возникает очень редко, но иногда это можно сделать из соображений эффективности или в отладочных целях.

Объекты-функции

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

template<class T,
class CmpFn> T &max(T &x1, T &x2, CmpFn cmp)
{
    return cmp(x1, x2) ? x1 : x2;
}

Что такое CmpFn? Естественнее всего предположить, что это указатель на функцию. Однако вызов функции по указателю - операция довольно долгая. В нашем примере вызов займёт больше времени, чем выполнение всех остальных инструкций в функции max. Проблема в том, что при таком подходе к передаче функции её не удаётся объявить как встроенную (inline).

Вместо указателя на функцию можно передать в max объект любого класса с перегруженным оператором (). При этом operator() можно объявить как встроенный, что при большом количестве обращений к max даст очевидный выигрыш в производительности.

Таким образом, объекты-функции используются в целях оптимизации

Предикаты

Термин "предикат" довольно часто фигурирует в книгах по STL. В действительности предикат - это просто функция (в частности объект-функция), которая возвращает bool. Различают унарные и бинарные предикаты. Унарные получают один параметр, бинарные - два.

Предикаты широко используются в STL. Унарные предикаты используются для задания подмножества элементов контейнера, удовлетворяющих некоторому условию. Например, функция count_if считает количество элементов последовательности, для которых заданный унарный предикат возвращает true. Бинарные предикаты чаще всего используются для сравнения двух элементов.

Адаптеры

Адаптер (adapter) - это класс, который не реализует собственную функциональность, а вместо этого предоставляет альтернативный интерфейс к функциональности другого класса.

В STL адаптеры применяются для самых различных классов (контейнеров, объектов-функций и т. д.). Наиболее типичный пример адаптера - стек. Стек может использовать в качестве нижележащего класса различные контейнеры (очередь, вектор, список), обеспечивая для них стандартный стековый интерфейс (функции push/pop).

После такой обстоятельной теоретической подготовки можно перейти к практическим аспектам работы с STL, которые я рассмотрю во второй части статьи.

/ / / / ОБРАТНАЯ СВЯЗЬ / / / / / / / / / / / / / /

Александру Шаргину от читателя пришло интересное письмо. Он решил, что его было бы полезно прочитать всем.

В N36 рассылки "Программирование на Visual C++" я прочитал ваш пример убивания чужого окна, использующий DLL. Однако, мне показалось, что пример не полный, поскольку может возникнуть ситуация, когда сразу несолько процессов вызовут функцию DLL KillWndNow. В этом случае может сложиться ситуация, когда сначала первый процесс запишет в shared-переменную hWndToKill хэндл убиваемого окна, затем второй процесс запишет тужа же, но уже другой хэндл, потом первый запустит механизм убивания, но в результате убьется окно, на которое уже успел указать второй процесс.

Более корректно было бы поместить весь код функции KillWndNow в критическую секцию. Но, поскольку критические секции могут использоваться только в рамках одного процесса, для синхронизации здесь уместно использоваться мютекс.

Еще один нюанс. В функции KillWndNow где-нибудь после строчки
WaitForSingleObject(hEvent, INFINITE);
нужно вставить оператор hWndToKill=NULL, иначе при любой загрузке DLL (например, другим процессом, который вызвал KillWndNow) в функции DllMain будет исполняться ветка кода, пытающаяся убивать окно, хотя фактически запрос на такую операцию не поступал.

И, наконец, последнее. Все это будет работать только в том случае, если эта DLL еще не была загружена процессом, окно которого мы хотим убить. А если была? Придется использовать какой-то другой механизм, или все-таки можно как-нибудь усовершенствовать существующий? Самое плохое даже не то, что чужое окно не будет убиваться, а то, что функция KillWndNow подвиснет на вызове WaitForSingleObject, поскольку это событие никогда не реализуется. А если еще и добавить мютекс, как я описал выше, то повиснут и все последующие вызовы этой функции, но уже на ожидании освобождения мютекса. - Dmitry Batsuro

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Остальных рубрик сегодня не будет. За неделю мне не пришло НИ ОДНОГО ответа на вопрос, так что я решил его оставить на следующую неделю.

До встречи!

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


No.38

Выпуск No. 38 от 24 марта 2001 г.

Приветствую!

Сегодня мы с вами углубимся в особенности систем Windows NT/2000 - а именно, научимся создавать под них особые программы, называемые службами или сервисами.

/ / / / СТАТЬЯ / / / / / / / / / / / / / / / / / / / / / /

Службы Windows NT: назначение и разработка
 Зачем и как создавать службы (сервисы) Windows NT/2000.

Автор: Михаил Плакунов
Источник: СофтТерра

Службы Windows NT, общие понятия

Служба Windows NT (Windows NT service) - специальный процесс, обладающий унифицированным интерфейсом для взаимодействия с операционной системой Windows NT. Службы делятся на два типа - службы Win32, взаимодействующие с операционной системой посредством диспетчера управления службами (Service Control Manager - SCM), и драйвера, работающие по протоколу драйвера устройства Windows NT. Далее в этой статье мы будем обсуждать только службы Win32.

Применение служб

Одним из важнейших свойств службы является неинтерактивность. Типичное <поведение> службы - это незаметная для обычного пользователя работа в фоновом режиме. В силу этого службы наиболее подходят для реализации следующих типов приложений:

  • Сервера в архитектуре клиент-сервер (например, MS SQL, MS Exchange Server)
  • Сетевые службы Windows NT (Server, Workstation);
  • Серверные (в смысле функциональности) компоненты распределенных приложений (например, всевозможные программы мониторинга).
Основные свойства служб

От обычного приложения Win32 службу отличают 3 основных свойства. Рассмотрим каждое из них.

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

Во-вторых, возможность запуска службы до регистрации пользователя и, как следствие, возможность работы вообще без зарегистрированного пользователя. Любая служба может быть запущена автоматически при старте операционной системы и начать работу еще до того как пользователь произведет вход в систему.

И, наконец, возможность работы в произвольном контексте безопасности. Контекст безопасности Windows NT определяет совокупность прав доступа процесса к различным объектам системы и данным. В отличие от обычного приложения Win32, которое всегда запускается в контексте безопасности пользователя, зарегистрированного в данный момент в системе, для службы контекст безопасности ее выполнения можно определить заранее. Это означает, что для службы можно определить набор ее прав доступа к объектам системы заранее и тем самым ограничить сферу ее деятельности. Применительно к службам существует специальный вид контекста безопасности, используемый по умолчанию и называющийся Local System. Служба, запущенная в этом контексте, обладает правами только на ресурсы локального компьютера. Никакие сетевые операции не могут быть осуществлены с правами Local System, поскольку этот контекст имеет смысл только на локальном компьютере и не опознается другими компьютерами сети.

Взаимодействие службы с другими приложениями

Любое приложение, имеющее соответствующие права, может взаимодействовать со службой. Взаимодействие, в первую очередь, подразумевает изменение состояния службы, то есть перевод ее в одно из трех состояний - работающее (Запуск), приостанов (Пауза), останов и осуществляется при помощи подачи запросов SCM. Запросы бывают трех типов - сообщения от служб (фиксация их состояний), запросы, связанные с изменением конфигурации службы или получением информации о ней и запросы приложений на изменение состояния службы.

Для управления службой необходимо в первую очередь получают ее дескриптор с помощью функции Win32 API OpenService. Функция StartService запускает службу. При необходимости изменение состояния службы производится вызовом функции ControlService.

База данных службы

Информация о каждой службе хранится в реестре - в ключе HKLM \ SYSTEM \ CurrentControlSet \ Services \ ServiceName. Там содержатся следующие сведения:

  • Тип службы. Указывает на то, реализована ли в данном приложении только одна служба (эксклюзивная) или же их в приложении несколько. Эксклюзивная служба может работать в любом контексте безопасности. Несколько служб внутри одного приложения могут работать только в контексте LocalSystem.
  • Тип запуска. Автоматический - служба запускается при старте системы. По требованию - служба запускается пользователем вручную. Деактивированный - служба не может быть запущена.
  • Имя исполняемого модуля (EXE-файл).
  • Порядок запуска по отношению к другим службам. В некоторых случаях для корректной работы службы требуется, чтобы была запущена одна или несколько других служб. В этом случае в реестре содержится информация о службах, запускаемых перед данной.
  • Контекст безопасности выполнения службы (сетевое имя и пароль). По умолчанию контекст безопасности соответствует LocalSystem.

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

  • OpenSCManager, CreateService, OpenService, CloseServiceHandle - для создания (открытия) службы;
  • QueryServiceConfig, QueryServiceObjectSecurity, EnumDependentServices, EnumServicesStatus - для получения информации о службе;
  • ChangeServiceConfig, SetServiceObjectSecurity, LockServiceDatabase, UnlockServiceDatabase, QueryServiceLockStatus - для изменения конфигурационной информации службы.
Внутреннее устройство службы.

Для того, чтобы <быть службой>, приложение должно быть устроено соответствующим образом, а именно - включать в себя определенный набор функций (в терминах C++) с определенной функциональностью. Рассмотрим кратко каждую из них.

Функция main

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

Функция ServiceMain

Помимо общепроцессной точки входа существует еще отдельная точка входа для каждой из служб, реализованных в приложении. Имена функций, являющихся точками входа служб (для простоты назовем их всех одинаково - ServiceMain), передаются SCM в одном из параметров при вызове StartServiceCtrlDispatcher. При запуске каждой службы для выполнения ServiceMain создается отдельный поток.

Получив управление, ServiceMain первым делом должна зарегистрировать обработчик запросов к службе, функцию Handler, свою для каждой из служб в приложении. После этого в ServiceMain обычно следуют какие-либо действия для инициализации службы - выделение памяти, чтение данных и т.п. Эти действия должны обязательно сопровождаться уведомлениями SCM о том, что служба все еще находится в процессе старта и никаких сбоев не произошло. Уведомления посылаются при помощи вызовов функции SetServiceStatus. Все вызовы, кроме самого последнего должны быть с параметром SERVICE_START_PENDING, а самый последний - с параметром SERVICE_RUNNING. Периодичность вызовов определяется разработчиком службы, исходя их следующего условия: продолжительность временного интервала между двумя соседними вызовами SetServiceStatus не должна превышать значения параметра dwWaitHint, переданного SCM при первом из двух вызовов. В противном случае SCM, не получив во-время очередного уведомления, принудительно остановит службу. Такой способ позволяет избежать ситуации <зависания> службы на старте в результате возникновения тех или иных сбоев (вспомним, что службы обычно неинтерактивны и могут запускаться в отсутствие пользователя). Обычная практика заключается в том, что после завершения очередного шага инициализации происходит уведомление SCM.

Функция Handler

Как уже упоминалось выше, Handler - это прототип callback-функции, обработчика запросов к службе, своей для каждой службы в приложении. Handler вызывается, когда службе приходит запрос (запуск, приостанов, возобновление, останов, сообщение текущего состояния) и выполняет необходимые в соответствии с запросом действия, после чего сообщает новое состояние SCM.

Один запрос следует отметить особо - запрос, поступающий при завершении работы системы (Shutdown). Этот запрос сигнализирует о необходимости выполнить деинициализацию и завершиться. Microsoft утверждает, что для завершения работы каждой службе выделяется 20 секунд, после чего она останавливается принудительно. Однако тесты показали, что это условие выполняется не всегда и служба принудительно останавливается до истечения этого промежутка времени.

Система безопасности служб

Любое действие над службами требует наличия соответствующих прав у приложения. Все приложения обладают правами на соединение с SCM, перечисление служб и проверку заблокированности БД службы. Регистрировать в сиситеме новую службу или блокировать БД службы могут только приложения, обладающие административными правами.

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

  • Все пользователи имеют права SERVICE_QUERY_CONFIG, SERVICE_QUERY_STATUS, SERVICE_ENUMERATE_DEPENDENTS, SERVICE_INTERROGATE и SERVICE_USER_DEFINED_CONTROL;
  • Пользователи, входящие в группу Power Users и учетная запись LocalSystem дополнительно имеют права SERVICE_START, SERVICE_PAUSE_CONTINUE и SERVICE_STOP;
  • Пользователи, входящие в группы Administrators и System Operators имеют право SERVICE_ALL_ACCESS.
Службы и интерактивность

По умолчанию интерактивные службы могут выполняться только в контексте безопасности LocalSystem. Это связано с особенностями вывода на экран монитора в Windows NT, где существует, например, такой объект как "Desktop", для работы с которым нужно иметь соответствующие права доступа, которых может не оказаться у произвольной учетной записи, отличной от LocalSystem. Несмотря на то, что в подавляющем большинстве случаев это ограничение несущественно однако иногда существует необходимость создать службу, которая выводила бы информацию на экран монитора и при этом выполнялась бы в контексте безопасности отличном от LocalSystem, например, серверная компонента приложения для запуска приложений на удаленном компьютере.

Следующий фрагмент кода иллюстрирует такую возможность.

// Функция, аналог MessageBox Win32 API
int ServerMessageBox(RPC_BINDING_HANDLE h, LPSTR lpszText,
 LPSTR lpszTitle, UINT fuStyle)
{
 DWORD dwThreadId;
 HWINSTA hwinstaSave;
 HDESK hdeskSave;
 HWINSTA hwinstaUser;
 HDESK hdeskUser;
 int result;

 // Запоминаем текущие объекты "Window station" и "Desktop".
 GetDesktopWindow();
 hwinstaSave = GetProcessWindowStation();
 dwThreadId = GetCurrentThreadId();
 hdeskSave = GetThreadDesktop(dwThreadId);

 // Меняем контекст безопасности на тот,
 // который есть у вызавшего клиента RPC
 // и получаем доступ к пользовательским
 // объектам "Window station" и "Desktop".
 RpcImpersonateClient(h);
 hwinstaUser = OpenWindowStation("WinSta0",
 FALSE, MAXIMUM_ALLOWED);
 if (hwinstaUser == NULL)
 {
 RpcRevertToSelf();
 return 0;
 }
 SetProcessWindowStation(hwinstaUser);
 hdeskUser = OpenDesktop("Default", 0, FALSE, MAXIMUM_ALLOWED);
 RpcRevertToSelf();
 if (hdeskUser == NULL)
 {
 SetProcessWindowStation(hwinstaSave);
 CloseWindowStation(hwinstaUser);
 return 0;
 }
 SetThreadDesktop(hdeskUser);

 // Выводим обычное текстовое окно.
 result = MessageBox(NULL, lpszText, lpszTitle, fuStyle);

 // Восстанавливаем сохраненные объекты
 // "Window station" и "Desktop".
 SetThreadDesktop(hdeskSave);
 SetProcessWindowStation(hwinstaSave);
 CloseDesktop(hdeskUser);
 CloseWindowStation(hwinstaUser);

 return result;
}

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

Пример службы (ключевые фрагменты)

Рассмотрим на примере ключевые фрагменты приложения на языке С++, реализующего службу Windows NT. Для наглядности несущественные части кода опущены.

Функция main

Вот как выглядит код функции main:

void main()
{
 SERVICE_TABLE_ENTRY steTable[] =
 {
 {SERVICENAME, ServiceMain},
 {NULL, NULL}
 };

 // Устанавливаем соединение с SCM. Внутри этой функции
 // происходит прием и диспетчеризация запросов.
 StartServiceCtrlDispatcher(steTable);
}
Функция ServiceMain

Особенностью кода, содержащегося в ServiceMain, является то, что часто невозможно заранее предсказать время выполнения той или иной операции, особенно, если учесть, что ее выполнение происходит в операционной системе с вытесняющей многозадачностью. Если операция продлится дольше указанного в параметре вызова SetServiceStatus интервала времени, служба не сможет во-время отправить следующее уведомление, в результате чего SCM остановит ее работу. Примерами потенциально <опасных> операций могут служить вызовы функций работы с сетью при больших таймаутах или единовременное чтение большого количества информации с медленного носителя. Кроме того, такой подход совершенно не применим при отладке службы, поскольку выполнение программы в отладчике сопровождается большими паузами, необходимыми разработчику.

Для преодоления этой проблемы все операции по взаимодействию с SCM следует выполнять в отдельном потоке, не зависящем от действий, происходящих на этапе инициализации.

Алгоритм корректного запуска службы, использующий вспомогательный поток:

void WINAPI ServiceMain(DWORD dwArgc, LPSTR *psArgv)
{
 // Сразу регистрируем обработчик запросов.
 hSS = RegisterServiceCtrlHandler(SERVICENAME, ServiceHandler);

 sStatus.dwCheckPoint = 0;
 sStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP |
 SERVICE_ACCEPT_PAUSE_CONTINUE;
 sStatus.dwServiceSpecificExitCode = 0;
 sStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
 sStatus.dwWaitHint = 0;
 sStatus.dwWin32ExitCode = NOERROR;

 // Для инициализации службы вызывается функция InitService();
 // Для того, чтобы в процессе инициализации система не
 // выгрузила службу, запускается поток, который раз в
 // секунду сообщает, что служба в процессе инициализации.
 // Для синхронизации потока создаётся событие.
 // После этого запускается рабочий поток, для
 // синхронизации которого также
 // создаётся событие.

 hSendStartPending = CreateEvent(NULL, TRUE, FALSE, NULL);

 HANDLE hSendStartThread;
 DWORD dwThreadId;

 hSendStartThread = CreateThread(NULL, 0, SendStartPending,
 NULL, 0, &dwThreadId);

 //Здесь производится вся инициализация службы.
 InitService();

 SetEvent(hSendStartPending);

 if(
 WaitForSingleObject(hSendStartThread, 2000)
 != WAIT_OBJECT_0)
 {
 TerminateThread(hSendStartThread, 0);
 }

 CloseHandle(hSendStartPending);
 CloseHandle(hSendStartThread);

 hWork = CreateEvent(NULL, TRUE, FALSE, NULL);

 hServiceThread = CreateThread(NULL, 0, ServiceFunc,
 0, 0, &dwThreadId);

 sStatus.dwCurrentState = SERVICE_RUNNING;

 SetServiceStatus(hSS, &sStatus);
}

// Функция потока, каждую секунду посылающая уведомления SCM
// о том, что процесс инициализации идёт. Работа функции
// завершается, когда устанавливается
// событие hSendStartPending.

DWORD WINAPI SendStartPending(LPVOID)
{
 sStatus.dwCheckPoint = 0;
 sStatus.dwCurrentState = SERVICE_START_PENDING;
 sStatus.dwWaitHint = 2000;

 // "Засыпаем" на 1 секунду. Если через 1 секунду
 // событие hSendStartPending не перешло
 // в сигнальное состояние (инициализация службы не
 // закончилась), посылаем очередное уведомление,
 // установив максимальный интервал времени
 // в 2 секунды, для того, чтобы был запас времени до
 // следующего уведомления.
 while (true)
 {
 SetServiceStatus(hSS, &sStatus);
 sStatus.dwCheckPoint++;
 if(WaitForSingleObject(hSendStartPending,
 1000)!=WAIT_TIMEOUT)
 break;
 }

 sStatus.dwCheckPoint = 0;
 return 0;
}

// Функция, инициализирующая службу. Чтение данных,
// распределение памяти и т.п.
void InitService()
{
 ...
}

// Функция, содержащая <полезный> код службы.
DWORD WINAPI ServiceFunc(LPVOID)
{
 while (true)
 {
 if (!bPause)
 {
 // Здесь содержится код, который как правило
 // выполняет какие-либо циклические операции...
 }

 if (WaitForSingleObject(hWork, 1000)!=WAIT_TIMEOUT)
 break;
 }

 return 0;
}
Функция Handler

А вот код функции Handler и вспомогательных потоков:

// Обработчик запросов от SCM
void WINAPI ServiceHandler(DWORD dwCode)
{
 switch (dwCode)
 {
 case SERVICE_CONTROL_STOP:
 case SERVICE_CONTROL_SHUTDOWN:
   ReportStatusToSCMgr(SERVICE_STOP_PENDING,
                             NO_ERROR, 0, 1000);
   hSendStopPending = CreateEvent(NULL, TRUE, FALSE, NULL);
   hSendStopThread = CreateThread(NULL, 0,
   SendStopPending, NULL, 0, & dwThreadId);
   SetEvent(hWork);
   if (WaitForSingleObject(hServiceThread,
         1000) != WAIT_OBJECT_0)
   {
       TerminateThread(hServiceThread, 0);
   }
   SetEvent(hSendStopPending);
   CloseHandle(hServiceThread);
   CloseHandle(hWork);
   if(WaitForSingleObject(hSendStopThread,
       2000) != WAIT_OBJECT_0)
   {
     TerminateThread(hSendStopThread, 0);
   }
   CloseHandle(hSendStopPending);

   sStatus.dwCurrentState = SERVICE_STOPPED;
   SetServiceStatus(hSS, &sStatus);
   break;

 case SERVICE_CONTROL_PAUSE:
   bPause = true;
   sStatus.dwCurrentState = SERVICE_PAUSED;
   SetServiceStatus(hSS, &sStatus);
   break;

 case SERVICE_CONTROL_CONTINUE:
   bPause = true;
   sStatus.dwCurrentState = SERVICE_RUNNING;
   SetServiceStatus(hSS, &sStatus);
   break;

 case SERVICE_CONTROL_INTERROGATE:
   SetServiceStatus(hSS, &sStatus);
   break;

 default:
   SetServiceStatus(hSS, &sStatus);
   break;
 }
}

// Функция потока, аналогичная SendStartPending
// для останова службы.
DWORD WINAPI SendStopPending(LPVOID)
{
 sStatus.dwCheckPoint = 0;
 sStatus.dwCurrentState = SERVICE_STOP_PENDING;
 sStatus.dwWaitHint = 2000;

 while (true)
 {
 SetServiceStatus(hSS, &sStatus);
 sStatus.dwCheckPoint++;
 if(WaitForSingleObject(hSendStopPending,
 1000)!=WAIT_TIMEOUT)
 break;
 }

 sStatus.dwCheckPoint = 0;
 return 0;
}

Для запросов "Stop" и "Shutdown" используется алгоритм корректного останова службы, аналогичный тому, который используется при старте службы, с той лишь разницей, что вместо параметра SERVICE_START_PENDING в SetserviceStatus передается параметр SERVICE_STOP_PENDING, а вместо SERVICE_RUNNING - SERVICE_STOPPED.

В идеале для запросов "Pause" и "Continue" тоже следует использовать этот подход. Любознательный читатель без труда сможет реализовать его, опираясь на данные примеры.

Заключение

В заключение хотелось бы отметить, что с переходом на Windows 2000 разработка служб не претерпела изменений. Службы по-прежнему остаются важной частью программного обеспечения на платформе Windows, что предоставляет разработчикам широкое поле деятельности.

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

Q| Хотелось бы побольше узнать о предварительном просмотре. В русской программе он смотрится инородным телом на своем иностранном языке. Можно ли его как-то настраивать под себя?
В этой же связи: не могу решить проблему.
В программе 3 меню и, соответственно, 3 панели инструментов, которые создал в Create. Переключая меню, вызываю ShowControlBar - прячу ненужные панели и показываю необходимую. Но после вызова PRINT PREVIEW, в окне появляются сразу все 3 панели инструментов.
Попутно: что означает AFX_IDS_PREVIEW_CLOSE в String Table? - Serg Petukhov

|A Отвечу по порядку.

1. Все языко-зависимые компоненты для печати и предварительного просмотра (панель инструментов, диалог и строки) в соответствии с идеологией MFC оформлены как ресурсы. Эти ресурсы лежат в файле MFC42.DLL, но программа будет искать их там только если они отсутствуют в головной программе. Если же программа статически линкуется с MFC, ресурсы для печати/предварительного просмотра берутся из файла afxprint.rc. Чтобы в этом всём убедиться, достаточно открыть rc-файл, сгенерённым визардом, и найти там строчки:

#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_RUS)
...
#include "afxprint.rc"  // printing/print preview resources
#endif

Теперь понятно, как поправить ситуацию.
- Копируем ресурсы из файла afxprint.rc (без окантовочных директив, то есть от строчки "// Printing Resources") в файл ресурсов нашей программы. При этом нужно проследить, чтобы новые ресурсы попали между директивами #ifdef APPSTUDIO_INVOKED и соответствующего #endif (иначе новые ресурсы нельзя будет изменить в редакторе).
- Убираем из файла ресурсов строчку #include "afxprint.rc" (вручную или через View->Resource includes). На самом деле, это необходимо сделать только при статической линковке с MFC, так как при динамической линковке эта строчка не используется (как я уже говорил, в этом случае ресурсы берутся из MFC42.DLL).
- Затем запускаем редактор ресурсов Visual Studio и русифицируем новые ресурсы. Не забудьте предварительно установить для каждого ресурса в свойствах Language:Russian, иначе вместо русского языка получите иероглифы!
- Пересобираем проект и убеждаемся, что теперь предварительный просмотр говорит по-русски.

2. После выхода из Print Preview запускается функция CView::OnEndPrintPreview (файл viewcore.cpp). Из неё вызывается ещё одна функция - CFrameWnd::OnSetPreviewMode (файл winfrm.cpp). Просмотрев код этой функции, нетрудно убедиться, что она делает видимыми все стандартные панели с идентификаторами от AFX_IDW_CONTROLBAR_FIRST до AFX_IDW_CONTROLBAR_FIRST+31 включительно. Таким образом, чтобы MFC не вмешивалась в вашу работу с панелями инструментов, нужно назначить им идентификаторы за пределами этого диапазона (например, AFX_IDW_CONTROLBAR_LAST-N, где N=0,1,2,...):
m_wndToolBar.CreateEx(..., AFX_IDW_CONTROLBAR_LAST);

3. Что касается строки AFX_IDS_PREVIEW_CLOSE, она просто содержит подсказку для команды Close предварительного просмотра. Если вам интересно, где она появляется, запустите режим предварительного просмотра, а затем наведите курсор на пункт Close из системного меню программы (которое раскрывается по щелчку на иконке в левом верхнем углу главного окна). При этом текст подсказки о закрытии предварительного просмотра появится в строке состояния. Можете заменить его на любой другой (на русском языке). - Александр Шаргин (rudankort@mail.ru)

/ / / В ПОИСКАХ ИСТИНЫ / / / / / / / / / / / / /

Q| Есть приложение на базе диалога. По некоторым причинам необходимо уже внутрь этого диалога вставить закладки (страницы свойств, как хотите). Все это нормально делается и проблем тут не возникает. Но вот при использовании клавиши Tab для прогулки по диалогу фокус с последнего контрола, не принадлежащего Property Page, перемещается не на закладку страницы, а на ее первый определенный в Tab Layout контрол, и только после пробегания по всем элементам Property Page попадает на закладку. Как это вылечить? - George Orlov

Ответить на вопрос

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Это все на сегодня. Счастливо!

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


No.39

Выпуск No. 39 от 1 апреля 2001 г.

Добрый день, уважаемые подписчики! С праздником вас!

/ / / / СТАТЬЯ / / / / / / / / / / / / / / / / / / / / / /

Диагностические средства MFC

Автор: Олег Быков

Библиотека MFC предоставляет программисту мощный набор средств для отладки приложений любой сложности. Данная статья ставит своей целью последовательное рассмотрение диагностических средств для помощи начинающим MFC-программистам в выборе и более полном их использовании.

Разработка коммерческих приложений всегда подразумевает написание стабильно работающих систем, и, как следствие, наличие в коде тотальной проверки всего на свете - входных параметров функций, возвращенных значений, полученных указателей и т.д. Но за стабильность приходится платить замедлением работы программы. MFC предлагает следующий подход к проблеме: разработчик вставляет в код набор диагностических макроопределений, которые при невыполнении заданных условий сообщают имя исходного файла с ошибкой, номер строки, и останавливают работу программы. При этом данные макроопределения выполняются только при отладочной сборке проекта (Debug build).

Иными словами, в код помещаются проверки, которые выполняются только в отладочной версии программы, и не включаются в код при окончательной сборке (Release build). За время работы с отладочной версией программы (в идеале) выясняются и устраняются все возможные ошибочные ситуации и надобность в замедляющих работу проверках отпадает (здесь не имеются в виду ошибки, на которые программа должна реагировать определенными действиями. В частности, не стоит проверять таким образом результаты работы API-функций, так как нельзя гарантировать корректность возвращаемых ими значений и в отладочной сборке, и в окончательной). Чтобы стало понятней, рассмотрим несколько диагностических макроопределений.

ASSERT и VERIFY

ASSERT - пожалуй, один из самых часто употребляемых макросов. Принимая в качестве аргумента булево значение, ASSERT продолжает работу программы, если это значение равно TRUE, и прерывает работу программы в ином случае. При этом ASSERT выводит информационное окно с именем исходного файла и номером строки, содержащей сработавший макрос, и предоставляет разработчику выбор - окончательно прервать работу программы (Abort), переключиться в окно отладчика (Retry) или продолжить работу (Ignore).

В качестве примера использования ASSERT можно привести проверку входного значения функции:

void CPerson::SetPersonAge(int nAge)
{
   ASSERT((nAge>=0) && (nAge<200)); // сработает при любых x, меньших 0
   m_nAge = x;			    // или больших 199
};

При срабатывании макроса (то есть, при передаче неверного nAge) у разработчика есть возможность переключиться в окно отладчика и через список вызовов (Call Stack) определить, откуда был передан ошибочный параметр.

ПРИМЕЧАНИЕ
ASSERT развернется в код только при Debug-сборке. Чтобы обеспечить вычисление параметра и в окончательной версии проекта (в случае, когда в ASSERT вызывается нужная функция), используйте макроопределение VERIFY. При Debug-сборке этот макрос полностью идентичен ASSERT, но, в отличие от него, при Release-сборке VERIFY разворачивается в код и вычисляет значение своего аргумента, хотя при этом никак не влияет на ход выполнения программы.

В MFC определен вспомогательный макрос DEBUG_ONLY, который служит для обеспечения выполнения своего параметра только при Debug-сборке. В Release-версии приложения выражение внутри DEBUG_ONLY будет проигнорировано.

ASSERT_KINDOF и ASSERT_VALID

Эти макросы предназначены для диагностики состояния объектов. ASSERT_KINDOF принимает два параметра - имя класса и указатель на объект - и срабатывает (прерывая выполнение программы подобно ASSERT) в случае, когда объект, переданный по указателю, не является объектом данного класса или одного из потомков данного класса. Пример использования макроса:

CPerson::CPerson(CPerson &newPerson)
{
   ASSERT_KINDOF(CPerson, &newPerson); // сработает, если в конструктор
                                      // был передан объект не того класса
};

ASSERT_KINDOF полностью идентичен следующей конструкции (для нашего примера):

ASSERT(newPerson.IsKindOf(RUNTIME_CLASS(CPerson)));

Для того, чтобы получить информацию о классе в процессе исполнения, этот класс должен быть унаследован от CObject (или одного из его потомков), и для него должны быть использованы макросы DECLARE_DYNAMIC(classname) и IMPLEMENT_DYNAMIC(classname,baseclass) (иначе обращение к ASSERT_KINDOF приведет к ошибке нарушения защиты). Это относится и к проверяемому объекту, и к классу.

ASSERT_VALID служит для проверки внутреннего состояния объектов. Этот макрос принимает один параметр - указатель на проверяемый объект - и проделывает с ним следующее: проверяет валидность указателя, проверяет его на равенство NULL, и вызывает функцию объекта AssertValid.

AssertValid реализована почти во всех классах MFC (унаследованных от CObject), но разработчик может реализовать ее и в своем классе, соблюдая определенные правила. Во-первых, AssertValid должна быть переопределенной виртуальной функцией класса CObject. Эта функция описана как const, поэтому внутри нее нельзя изменять данные класса. Во-вторых, для индикации факта невалидности объекта функция должна использовать макрос ASSERT. И в-третьих, в AssertValid желательно вызвать эту же функцию класса-родителя.

Таким образом, разработчик может использовать ASSERT_VALID для реализации любых алгоритмов проверки состояния объекта. Например, вот так:

void CPerson::AssertValid() const
{
   CObject::AssertValid(); // подразумевается, что CPerson унаследован
                 // от CObject
   ASSERT((m_nAge>=0) && (m_nAge<200));
};
ПРИМЕЧАНИЕ
ASSERT_KINDOF и ASSERT_VALID развернутся в код только при Debug-сборке.

В MFC определены два вспомогательных макроса для тестирования указателей: ASSERT_POINTER и ASSERT_NULL_OR_POINTER. Оба они принимают в качестве параметров два значения - указатель и его тип. ASSERT_POINTER сначала проверяет указатель на NULL, затем тестирует память по этому указателю на валидность. По непрохождении хотя бы одной проверки макрос срабатывает и останавливает работу программы. ASSERT_NULL_OR_POINTER также проверяет память, на которую ссылается указатель, но не прерывает выполнение программы, если тестируемый указатель равен NULL (хотя указатель при этом и является невалидным).

Работа с отладочной информацией (Output window)

При отладке приложений у разработчика часто возникает необходимость узнать значение какой-нибудь переменной или результат, возвращенный функцией. Для этого используют либо пошаговое исполнение интересующего участка кода с просмотром переменных в Watch-окне отладчика, либо вывод информации с помощью информационных окон (функции MessageBox и AfxMessageBox).

В Windows предусмотрен еще один способ получить информацию от программы во время ее исполнения - функция OutputDebugString. Функция принимает LPCTSTR-строку и посылает ее отладчику, под управлением которого исполняется приложение. В случае запуска приложения из Visual C++ посланная строка попадает в окно Output последнего (закладка Debug). Преимущество данного способа в том, что он не требует прерывать работу программы для отслеживания значений (что иногда критично - например, при отлаживании обработчика сообщения WM_TIMER или асинхронного выполнения функций), и не требует убирать лишний код после отлаживания нужного участка (в отличие, скажем, от метода с MessageBox).

OutputDebugString можно использовать следующим образом:

void CPerson::SetPersonAge(int nAge)
{
   ASSERT((nAge>=0) && (nAge<200));
   m_nAge = x;
   CString str;
   str.Format(_T("New age = %d\n"),nAge);
   OutputDebugString(str);
};

MFC упрощает вывод отладочной информации, определяя глобальный объект afxDump класса CDumpContext. Информация при этом выводится таким образом:

void CPerson::SetPersonAge(int nAge)
{
   ASSERT((nAge>=0) && (nAge<200));
   m_nAge = x;
   afxDump << _T("New age = ") << nAge << _T("\n");
};

Отладочную информацию можно также выводить макросами TRACE, TRACE0, TRACE1, TRACE2 и TRACE3 (для таких целей обычно их и используют). Все они выводят переданную им информацию через afxDump, при этом принимают те же параметры, что и функция printf:

void CPerson::SetPersonAge(int nAge)
{
   ASSERT((nAge>=0) && (nAge<200));
   m_nAge = x;
   TRACE(_T("New age = %d\n"),nAge);
};

Макросы TRACEn аналогичны макросу TRACE, с той лишь разницей, что их первый параметр имеет тип LPCSTR (а не LPCTSTR), и они принимают не произвольное число параметров, а определенное цифрой в их имени. Длина первого параметра всех TRACE-макросов (после всех подстановок) не должна превышать 512 символов, иначе макрос сгенерирует ASSERT.

Макросы TRACEn оставлены в MFC для обратной совместимости, при написании приложений рекомендуется пользоваться макросом TRACE.

ПРИМЕЧАНИЕ
Вывод отладочной информации и через afxDump, и через TRACE-макросы работает только в Debug-версии приложения.

В поставку Visual Studio 6.0 входит утилита для настройки вывода информации через TRACE-макросы - "MFC Tracer" (tracer.exe). С ее помощью можно отключить вывод отладочной информации (даже при Debug-сборке), заставить MFC выводить перед каждым сообщением в окне Output имя сгенерировавшего это сообщение проекта (полезно при отладке проекта, использующего DLL или состоящего из нескольких приложений), включить вывод уведомлений MFC об обработке определенных оконных сообщений и т.д.

Заканчивая обсуждение отладочной информации, нельзя не упомянуть утилиту TRACEWIN, написанную Paul DiLascia, также доступную в исходных текстах в апрельском номере MSJ за 1997 год. Эта утилита внедряется во все запускаемые процессы, и для MFC Debug-проектов перенаправляет весь TRACE-вывод в отдельное окно (причем перенаправление автоматически включается даже для уже запущенных приложений). Очень удобный инструмент. Более того, в той же статье Paul DiLascia доходчиво разъясняет принципы внедрения DLL в чужой процесс и приводит C++-класс, облегчающий эту задачу.

Вывод информации о внутреннем состоянии объектов

afxDump позволяет выводить в окне отладчика не только переменные, но и целые объекты (порожденные от CObject). Конструкция afxDump << &myPerson (или afxDump << myPerson) развернется в вызов myPerson.Dump(afxDump) (виртуальная функция класса CObject). Разрабатывая собственный класс, программист может переопределить эту функцию, реализовав свой метод вывода внутренней информации объекта, например, вот так:

// CPerson унаследован от CObject
void CPerson::Dump(CDumpContext &dc) const
{
   CObject::Dump(dc);
   dc << T("Age = ") << m_nAge;
};

Вызов родительской функции CObject::Dump(dc) выведет на контекст имя класса, в случае, если для реализации этого класса (CPerson в примере выше) используется связка макросов DECLARE_DYNAMIC/IMPLEMENT_DYNAMIC (или DECLARE_SERIAL/IMPLEMENT_SERIAL). Обратите внимание, что посылка символа перевода строки в переопределенной Dump не требуется.

ПРИМЕЧАНИЕ
Вывод отладочной информации об объекте через функцию Dump будет работать только в Debug-версии приложения, но здесь разработчик должен сам позаботиться о выполнении этого ограничения - и объявление, и реализацию, и вызовы функции Dump следует обрамлять проверками на Debug-сборку проекта:
class CPerson : public CObject
{
   . . .
public:
#ifdef _DEBUG
   void Dump(dc) const;
   void AssertValid() const; // это же касается объявления AssertValid
                   // (но не использования макроса ASSERT_VALID !)
#endif
   . . .
};

После выполнения всех этих действий остается только скинуть в afxDump указатель на наш объект, и изучать полученную информацию в Output-окне отладчика. Многие классы MFC реализуют функцию Dump для диагностики их внутреннего состояния, что особенно полезно при отладке работы с классами-коллекциями C*Array, C*List и C*Map. Чтобы получить и состояние объектов, содержащихся в коллекции, нужно установить глубину вызова Dump-функции, отличную от 0, функцией SetDepth(int newDepth) класса CDumpContext:

CArray<CPerson,CPerson> arrPersons;
WorkWithArray(arrPersons); // здесь идет работа с массивом
#ifdef _DEBUG
   afxDump.SetDepth(1);    // вывести информацию не только о коллекции,
   afxDump << &arrPersons; // но и о всех ее членах (будет вызвана
                   // CPerson::Dump для каждого элемента массива)
#endif
Диагностика ошибок работы с памятью

Одна из самых распространенных ошибок при работе с памятью - выделение блока памяти (к примеру, при создании нового объекта) без его последующего освобождения (так называемые утечки памяти). Сами по себе эти утечки нефатальны ни для работы приложения, ни для работы системы (по завершению работы приложения Windows все равно освободит все занятые приложением блоки памяти), но это может привести к нехватке памяти, если приложение исполняется относительно долгое время (например, если это WinNT-сервис). Обычно для отслеживания утечек памяти используют специализированные программы, например, NuMega BoundsChecker, но и в MFC предусмотрены некоторые возможности для диагностики подобных ситуаций.

Класс CMemoryState предназначен для обнаружения динамически выделенных и не освобожденных впоследствии блоков памяти. Алгоритм работы с этим классом сводится к запоминанию списка созданных объектов функцией CMemoryState::Checkpoint, и последующим сравнением двух классов функцией CMemoryState::Difference. Например, вот так:

#ifdef _DEBUG
   CMemoryState msStart, msEnd, msDiff;
   msStart.Checkpoint();       // начало подозрительного блока
#endif
. . .
   CPerson *pPerson = new CPerson();
. . .
#ifdef _DEBUG
   msEnd.Checkpoint();         // конец подозрительного блока
   if (msDiff.Difference(msStart,msEnd)
   {
      TRACE0("Memory leaked!\n");
      msDiff.DumpAllObjectsSince(); //в Output-окне отладчика выведется
      msDiff.DumpStatistics();      //информация о созданных объектах
                    //и о динамической памяти вообще
   };
#endif
ПРИМЕЧАНИЕ
Обратите внимание на скобки #ifdef/endif - с классом CMemoryState можно работать только в Debug-версии библиотеки MFC.

Разработчики используют класс CMemoryState для проверки подозрительных кусков кода на корректность работы с динамической памятью. Библиотека MFC имитирует использование CMemoryState с помощью глобального объекта класса _AFX_DEBUG_STATE, в деструкторе которого вызывается функция _CrtDumpMemoryLeaks (подробнее об этом можно почитать в статье "Обнаружение и локализация утечек памяти").

Функции DumpAllObjectsSince и DumpStatistics выводят в окне отладчика информацию о всех выделенных объектах со времени последнего вызова Checkpoint() и информацию о состоянии динамической памяти, соответственно. Информация о памяти выводится в следующем виде:

0 bytes in 0 Free Blocks
22 bytes in 1 Object Blocks
45 bytes in 4 Non-Object Blocks
Largest number used: 67 bytes
Total allocations: 67 bytes

Первая строка показывает число блоков памяти в объектах с отложенным удалением (в MFC имеется способ сделать так, чтобы delete не удаляла объекты сразу, а откладывала бы эту процедуру до конца работы программы. Это делается для тестирования программ в условиях нехватки памяти). Вторая и третья строки показывают размер занимаемой памяти и число объектов, соответственно, порожденных и не порожденных от CОbject. Последние две строки показывают максимальный и общий размер выделенной памяти.

Для того, чтобы MFC включила в отчет о состоянии памяти имя файла и номер строки, на которой был выделен неосвобожденный объект, в программе должен присутствовать следующий код:

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

Эти строки MFC по умолчанию вставляет в исходные файлы при генерации нового проекта.

Надеюсь, этот обзор помог читателю сориентироваться в многообразии отладочных средств библиотеки MFC. Более подробную информацию по данной тематике можно найти в MSDN или исходных кодах примеров (в том числе исходных кодах самой MFC). Удачи!

Автор выражает благодарность Александру Шаргину за ценные советы и замечания.

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

Ну, господа, пришло время что-то решать... Так как мне опять не пришло ни одного ответа на вопрос, думаю что рубрика "Вопрос-Ответ", в том виде в каком она сейчас существует вам не интересна. Поэтому со следующего выпуска и вопросы, и ответы будут публиковаться одновременно. Это будет больше похоже на HOWTO.

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Это все на сегодня. Пока!

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


No.40

Выпуск No. 40 от 15 апреля 2001 г.

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

Сегодня я хочу сделать очень важное объявление. Важное потому, что касается рассылки в целом и непосредственно каждого из вас.

Начиная с сентября 2000 года, подписчики не переставали спрашивать меня о том, есть ли у рассылки сайт, где можно посмотреть архив выпусков и другие материалы по программированию. Сайта тогда не было, и я отвечал соответственно, что собираюсь его сделать. Он тогда планировался просто как сайт рассылки и ничего больше. Однако чуть позже у нас с Александром Шаргиным возникла идея сделать полноценный сайт для наших разработчиков. Вернее, идея эта витала в воздухе, ей была фактически пропитана каждая конференция по программированию.

И мы решили попробовать ее реализовать. Работа над этим проектом началась несколько месяцев назад, и за это время к нам примкнуло большое количество людей. Сейчас над сайтом работает целая команда.

Вы наверное уже поняли к чему я веду. ;) Но вы ошибаетесь, все еще полагая, что у рассылки теперь появился сайт. Потому что все как раз наоборот - у сайта теперь есть рассылка! Потому как сайт превратился поистине в глобальное начинание. И начинание это называется RSDN - Russian Software Developer Network.

Cайт проекта RSDN отныне и всегда доступен по адресу www.rsdn.ru или просто rsdn.ru

В настоящий момент RSDN состоит из шести основных разделов. В будущем их число, возможно, будет увеличиваться.

  • Проект RSDN. В этом разделе собраны страницы, относящиеся к сайту в целом. Новости сайта, рассылки, авторы, контактная информация - все это вы найдете в этом разделе.
  • Статьи. Здесь вы найдете библиотеку статей различной тематики. Вы сможете узнать много нового про различные технологии (Win32, COM, ADO), библиотеки классов (MFC, ATL, WTL), инструменты (Visual C++) и языки программирования (C, C++).
  • Вопросы и ответы (Q&A). В этом разделе собраны вопросы, которые наиболее часто задаются как начинающими, так и более продвинутыми программистами. На каждый вопрос приводится исчерпывающий ответ, сопровождаемый пояснениями, фрагментами кода, а в некоторых случаях и демонстрационным проектом.
  • Форумы. В этом разделе собраны форумы по самым различным тематикам, в которых вы можете обмениваться опытом с коллегами, разбросанными по всему земному шару. Если у вас есть вопрос, задайте его, и вам непременно помогут.
  • Файлы. Здесь вы найдете самые различные утилиты, библиотеки, классы, готовые компоненты и множество других полезных файлов, доступных для скачивания.
  • Ресурсы. В этом разделе содержится описание самых различных ресурсов, которые помогут вам в нелегкой работе. Вас ждет огромная коллекция ссылок на другие сайты, книги на русском и иностранных языках, доступные в бумажном и электронном виде, журналы, рассылки и конференции. Словом, этот раздел может служить отправной точкой для поиска необходимой информации.

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

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

Пара слов о рассылке. Рассылка теперь стала частью проекта RSDN. Это значит, что начиная со следующей недели, в ней будут публиковаться самые отборные и интересные материалы сайта, посвященные программированию на Visual C++. А уже сейчас на сайт выложен полный архив рассылки, где собраны практически все выпуски, вышедшие за время ее существования.

Что еще хочу заметить. На нашем сайте НЕТ БАННЕРОВ. Это полностью некоммерческий проект, именно поэтому вы не найдете на наших страницах никакой рекламы.

И еще - наше специальное предложение. RSDN - некоммерческий проект, и таковым и останется. Его авторы порой находятся в разных уголках Земли. Мы приглашаем и вас присоединиться к нашей команде. Мы обладаем технической возможностью разместить значительно большее количество материалов и разделов. У сайта есть хороший движок, отличный дизайн, форум, качественный хостинг, ведется разработка новых сервисов, все это будет и в вашем распоряжении, если вы решите примкнуть к RSDN. Например, вы могли бы открыть новый раздел на RSDN и стать его ведущим. Объединившись, значительно проще раскручивать и развивать ресурс.

Если вас заинтересовало это предложение, обращайтесь за дополнительной информацией по адресу team@rsdn.ru. Мы открыты для любых начинаний.

Ну ладно, что дальше говорить, когда надо смотреть! Итак, приглашаю всех познакомиться с новым сайтом, созданным программистами для программистов, познакомиться с RSDN!

Алекс Jenter jenter@rsdn.ru
RSDN Developer


No.41

Выпуск No. 41 от 22 апреля 2001 г.

Добрый день, уважаемые подписчики!

Ну что ж, похоже старт у проекта RSDN получился достаточно хороший, особенно если судить по вашим откликам. Большое спасибо всем, кто заглянул на сайт - я уверен, что вы не остались разочарованы. А тем, кто еще не видел RSDN, настоятельно рекомендую посмотреть - наверняка вы найдете что-то интересное для себя. К тому же теперь на сайте появилась полноценная поисковая система.

Как я и обещал, с этого выпуска рассылка начинает публиковать статьи из RSDN, касающиеся программирования на Visual C++. Но не надо думать, что рассылка будет вам бесполезной, если вы регулярно читаете статьи на сайте. Рассылка будет экономить ваши усилия и ваше время; кроме того, некоторые статьи в рассылке будут появляться даже раньше, чем на сайте.

/ / / / СТАТЬЯ / / / / / / / / / / / / / / / / / / / / / /

Использование парсера MSXML для работы с XML-документами

Автор: Кен Скрибнер (Kenn Scribner)
Перевод: Александр Шаргин
Источник: "Visual C++ Developer", Ноябрь 2000

Демонстрационный проект XMLNodeExerciser

Парсер MSXML основывается на объектной модели документа XML (XML Document Object Model, XML DOM). Поэтому важно в первую очередь рассмотреть различные объекты, связанные с документом. Они приведены в таблице 1. Эти объекты позаимствованы прямо из спецификаций XML. MSXML предпренимает дополнительные усилия для стыковки объектов XML DOM с моделью COM. Благодаря этому достаточно просто установить, какому объекту модели XML DOM соответствует тот или иной COM-интерфейс MSXML. Например, IXMLDOMNode представляет DOM-объект Node (узел).

Объект DOM | Назначение
DOMImplementation | Объект, который можно запросить об уровне поддержки модели DOM
DocumentFragment | Представляет часть дерева (хорошо подходит для операций Вырезать/Вставить)
Document | Представляет узел верхнего уровня в дереве
NodeList | Объект-итератор для доступа к узлам XML
Node | Расширяет базовое понятие помеченного элемента (tagged element) в XML
NamedNodeMap | Поддержка пространства имён и итерации для коллекций атрибутов
CharacterData | Объект для манипулирования текстом
Attr | Представляет атрибут(ы) элемента
Element | Узел, представляющий элемент XML (удобен для доступа к атрибутам)
Text | Представляет текст, содержащийся в элементе или атрибуте
CDATASection | Используется для отключения разбора и валидации некоторых разделов XML
Notation | Сожержит нотацию, расположенную в DTD (Document Type Definition, описание типов документа) или в схеме
Entity | Представляет разобранную или неразобранную сущность
EntityReference | Представляет узел, ссылающийся на некоторую сущность
ProcessingInstruction | Представляет инструкцию обработки
Таблица 1. Объекты XML DOM и их использование

Иногда это может сбивать с толку, но объекты XML-документа могут быть (и часто бывают) полиморфными. Так, узел (Node) в то же самое время является элементом (Element). Это вносит путаницу, когда вы решаете, какой объект DOM требуется для совершения некоторого действия. Вы создаёте узлы, используя объект документа (Document), но если вам требуется добавить атрибуты к только что созданному узлу, вам придётся поработать с ним как с одним из элементов. Если в отношениях между объектами и действиями над ними и существует какая-то закономерность, мне пока не удалось открыть её в процессе каждодневной работы. Я постоянно обращаюсь к документации в MSDN, чтобы посмотреть, какой интерфейс предоставляет методы, нужные мне для решения той или иной задачи. Методы различных объектов логически сгруппированы, и, по-видимому, именно этот принцип (группировка логически связанных операций) был использован при проектировании DOM.

Таким образом, весь фокус состоит в том, чтобы получить у парсера MSXML нужный DOM-объект, реализацию которого предоставляет объект COM. Обычная последовательность действий подразумевает создание COM-объекта самого MSXML, у которого затем можно запросить (или получить каким-то другим способом) указатели на другие объекты XML DOM (которые в свою очередь тоже являются COM-объектами).

Демонстрационное приложение, использующее XML DOM

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

  • Загружает XML-файл с диска.
  • Отыскивает определённый узел и добавляет к нему дочерний узел.
  • Находит ещё один узел и отображает содержащийся в нём текст.
  • Сохраняет изменённый документ на диск.

Чтобы ещё больше упростить задачу, я жёстко "зашил" в программу имена XML-файлов и узлов. Понятно, что в реальном приложении вы вряд ли примените эту тактику. Но в нашем случае она имеет смысл, так как ещё больше упрощает код, связанный с использованием MSXML.

Как и во многих других случаях, я использовал в своём примере библиотеку ATL как удобную обёртку для всех операций, связанных с COM. Поэтому вы непременно увидете, как я использую объекты CComPtr и CComQIPtr. Для ровного счёта я добавил к ним также объекты CComBSTR и CComVariant. Если они вам не знакомы, просто запомните, что они являются шаблонами и сами заботятся о многих деталях, которые для наших целей несущественны. Для нас важно рассмотреть, каким образом искать узлы XML, добавлять новые узлы и отображать содержащийся в них текст.

Моё консольное приложение будет загружать XML-документ под названием xmldata.xml (предполагается, что он лежит в одном каталоге с исполняемым файлом), содержащий следующие данные:

<?xml version="1.0"?>
<xmldata>
   <xmlnode />
   <xmltext>Hello, World!</xmltext>
</xmldata>

Сначала мы будем искать узел xmlnode, и если найдём, добавим к нему новый узел (с атрибутом) в качестве дочернего. В результате получится документ следующего вида:

<?xml version="1.0"?>
<xmldata>
   <xmlnode>
      <xmlchildnode xml="fun" />
   </xmlnode>
   <xmltext>Hello, World!</xmltext>
</xmldata>

Далее мы напечатаем сообщение, содержащееся в узле xmltext ("Hello, World!"), и сохраним полученный документ в файл updatedxml.xml. После этого вы сможете посмотреть результаты, используя текстовый редактор или Internet Explorer 5.x. Давайте займёмся кодом.

Прежде всего приложение инициализирует библиотеку COM, а затем создаёт экземпляр парсера MSXML:

CComPtr<IXMLDOMDocument> spXMLDOM;
HRESULT hr = spXMLDOM.CoCreateInstance(__uuidof(DOMDocument));

if ( FAILED(hr) )
   throw "Unable to create XML parser object";
if ( spXMLDOM.p == NULL )
   throw "Unable to create XML parser object";

Если нам удалось создать экземпляр парсера, мы загружаем в него XML-документ:

VARIANT_BOOL bSuccess = false;
hr = spXMLDOM->load(CComVariant(L"xmldata.xml"), &bSuccess);

if ( FAILED(hr) )
   throw "Unable to load XML document into the parser";
if ( !bSuccess )
   throw "Unable to load XML document into the parser";

Поиск узла осуществляется через объект документа, поэтому мы используем IXMLDOMDocument::selectSingleNode() для обнаружения нужного узла по его имени. Есть и другие способы, но этот наиболее прост, в том случае, если вы точно знаете, какой узел вам требуется.

CComBSTR bstrSS(L"xmldata/xmlnode");
CComPtr<IXMLDOMNode> spXMLNode;
hr = spXMLDOM->selectSingleNode(bstrSS,&spXMLNode);

if ( FAILED(hr) )
   throw "Unable to locate 'xmlnode' XML node";
if ( spXMLNode.p == NULL )
   throw "Unable to locate 'xmlnode' XML node";

Другие методы, о которых вам следует знать, - это IXMLDOMDocument::nodeFromID() и IXMLDOMElement::getElementsByTagName(), которые вы можете использовать, чтобы получить список узлов в документе. Вы также можете обратиться к документу как к дереву и просканировать его (получая дочерний узел, все узлы одного уровня и т. д.).

В любом случае, результатом поиска станет объект узла MSXML, IXMLDOMNode. Узел должен существовать где-то в документе, иначе поиск закончится неудачей. Моё приложение использует его как родителя для совершенно нового узла, который создаётся объектом XML-документа:

CComPtr<IXMLDOMNode> spXMLChildNode;
hr = spXMLDOM->createNode(CComVariant(NODE_ELEMENT),
                          CComBSTR("xmlchildnode"),
                          NULL,
                          &spXMLChildNode);

if ( FAILED(hr) )
   throw "Unable to create 'xmlchildnode' XML node";
if ( spXMLChildNode.p == NULL )
   throw "Unable to create 'xmlchildnode' XML node";

Если парсеру удалось создать новый узел, следующий шаг - разместить его в дереве XML. Метод IXMLDOMNode::appendChild() - как раз то, что нам нужно.

CComPtr<IXMLDOMNode> spInsertedNode;
hr = spXMLNode->appendChild(spXMLChildNode,&spInsertedNode);

if ( FAILED(hr) )
   throw "Unable to move 'xmlchildnode' XML node";
if ( spInsertedNode.p == NULL )
   throw "Unable to move 'xmlchildnode' XML node";

Если родительский узел принял только что созданный узел в качестве дочернего, он вернёт вам ещё один экземпляр IXMLDOMNode, который представляет новый узел. На самом деле, этот новый узел и узел, который вы передали в appendChild(), в точности совпадают. Тем не менее, проверка указателя на добавленный дочерний узел может быть полезной, так как в случае ошибки он примет значение NULL.

Итак, мы уже нашли требуемый узел и добавили к нему дочерний узел; теперь посмотрим, как работать с атрибутами. Представьте себе, что вам нужно добавить к новому дочернему узлу атрибут:

xml="fun"

Сделать это не сложно, но вам придётся переключиться с IXMLDOMNode на IXMLDOMElement, чтобы поработать с узлом как с элементом. На практике это означает, что вам придётся запросить у интерфейса IXMLDOMNode связанный с ним интерфейс IXMLDOMElement, а потом, получив его, вызвать IXMLDOMElement::setAttribute():

CComQIPtr<IXMLDOMElement> spXMLChildElement;
spXMLChildElement = spInsertedNode;
if ( spXMLChildElement.p == NULL )
   throw "Unable to query for 'xmlchildnode' XML element interface";

hr = spXMLChildElement->setAttribute(CComBSTR(L"xml"),CComVariant(L"fun"));
if ( FAILED(hr) )
   throw "Unable to insert new attribute";

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

Для извлечение данных предназначен метод IXMLDOMNode::get_nodeTypedValue(). Данные, которые содержит узел, можно задавать с использованием схемы типов фирмы Microsoft, поэтому вы без труда можете сохранять числа с плавающей точкой, целые числа, строки или любые другие поддерживаемые схемой данные. Тип данных задаётся с использованием атрибута dt:type, например:

<model dt:type="string">SL-2</model>
<year dt:type="int">1992</year>

Если некоторый узел содержит данные заданного типа, вы сможете извлечь их в нужном формате, используя get_nodeTypedValue(). Если тип не задан, по умолчанию он считается текстовым, и парсер вернёт вам VARIANT с содержащимся в нём BSTR. В нашем случае этого достаточно, поскольку узел, который мы ищем, является текстовым и действительно содержит строку. Если нужно, мы всегда сможем отконвертировать её в другое представление, используя средства типа atoi(). А пока просто извлечём строку и отобразим её.

CComVariant varValue(VT_EMPTY);
hr = spXMLNode->get_nodeTypedValue(&varValue);
if ( FAILED(hr) )
   throw "Unable to retrieve 'xmltext' text";

if ( varValue.vt == VT_BSTR ) {
   // Display the results... since we're not using the
   // wide version of the STL, we need to convert the
   // BSTR to ANSI text for display...

   USES_CONVERSION;
   LPTSTR lpstrMsg = W2T(varValue.bstrVal);
   std::cout << lpstrMsg << std::endl;
}
else {
   // Some error
   throw "Unable to retrieve 'xmltext' text";
}

Если нам удалось извлечь значение, связанное с узлом, и если оно оказалось именно того типа, который мы ожидаем (BSTR), мы выводим текст на экран. В противном случае просто выводится сообщение об ошибке. Но вы, в зависимости от ситуации, можете предпринять и другие действия.

Наша последняя задача - сохранить обновлённое XML-дерево на диск, что мы и делаем, используя IXMLDOMDocument::save():

hr = spXMLDOM->save(CComVariant("updatedxml.xml"));
if ( FAILED(hr) )
   throw "Unable to save updated XML document";

Сохранив документ, программа выдаёт на экран короткое сообщение и завершается.

Эта демонстрационная программа вряд ли поразит ваше воображение. Вы могли бы сделать ещё очень много, но я надеюсь, что этот простой пример показал вам, как использовать MSXML в программах на языке C++. Сам по себе парсер - сложный продукт, и я настоятельно рекомендую вам использовать MSDN как справочное руководство по нему. Парсер предоставляет множество интерфейсов, каждый из которых обычно содержит большое количество методов. Несмотря на это, я широко использую парсер в своих проектах и теперь, поработав и поэкспериментировав с ним, нахожу его простым и удобным в использовании. Я надеюсь, что и вы найдёте ему, а также XML в целом, множество применений.

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

Как разрешить перетаскивание окна за любую точку?

Автор: Алексей Кирюшкин

Демонстрационное приложение DragWin

Пример - приложение DragWin (диалоговое окошко, MFC) иллюстрирует два способа осуществить перемещение окна с захватом его не только за заголовок, но и за любую точку на клиентской области. Идея первого способа проста - при получении сообщения о перемещении мыши передвигаем наше окно в соответствии с новыми координатами. Второй способ поизящнее, и заключается в некотором "обмане" Windows, после которого она считает, что мышь находится над заголовоком окна, даже если реально это уже клиентсткая часть.

Способ 1

Реализован для главного окна приложения. Заключается в написании собственных обработчиков нажатия (WM_LBUTTONDOWN), перемещения (WM_MOUSEMOVE) и отпускания (WM_LBUTTONUP) левой кнопки мыши. Обработчики на данные события устанавливаются стандартным образом - через MFC ClassWizard.

void CDragWinDlg::OnLButtonDown(UINT nFlags, CPoint point)
{
    // выставим флажок - пошло перетаскивание
    m_bMoveWindow = TRUE;
    // все сообщения от мыши - к нашему окну, независимо от координат
    // чтобы мышь не улетала с окна при быстром движении
    SetCapture();
    // сохраняем координаты окна
    GetWindowRect(m_RectDlg);
    // сохраняем положение мышки внутри окна программы
    ClientToScreen(&point);
    m_MouseInDlg = point - m_RectDlg.TopLeft();
    // меняем курсор, чтоб веселее было тащить
    m_hCursor = m_hCursorDown;
    ::SetCursor(m_hCursor);

    // вызываем обработчик по умолчанию
    CDialog::OnLButtonDown(nFlags, point);
}

void CDragWinDlg::OnMouseMove(UINT nFlags, CPoint point)
{
    if (m_bMoveWindow) // надо тащить
    {
        // преобразуем координаты мыши в экранные
        // именно они нужны будут для SetWindowPos()
        ClientToScreen(&point);
        // двигаем окно в соответствии с новыми координатами мыши
        SetWindowPos(&wndTop,
            point.x - m_MouseInDlg.x, point.y - m_MouseInDlg.y,
            m_RectDlg.right - m_RectDlg.left,
            m_RectDlg.bottom - m_RectDlg.top,
            SWP_SHOWWINDOW);
        // поскольку обработчик по умолчанию все равно будет использовать
        // первоначальные параметры сообщения
        // обратное преобразование ScreenToClient(&point);
        // можно не вызывать
    }

    // вызываем обработчик по умолчанию
    CDialog::OnMouseMove(nFlags, point);
}

void CDragWinDlg::OnLButtonUp(UINT nFlags, CPoint point)
{
    // перетаскивание закончилось
    m_bMoveWindow = FALSE;

    // "отпускаем" мышку
    ReleaseCapture();

    // меняем курсор на исходный
    m_hCursor = m_hCursorUp;

    // вызываем обработчик по умолчанию
    CDialog::OnLButtonUp(nFlags, point);
}

BOOL CDragWinDlg::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
{
    // заменяем курсор на свой
    ::SetCursor(m_hCursor);

    return TRUE;
    // !!! было return CDialog::OnSetCursor(pWnd, nHitTest, message);
}

Замена курсора естесственно не является критичной для собственно перетаскивания, а добавлена исключительно для визуализации процесса захвата окошка.

Способ 2

Реализован для окна About этого же приложения. Заключается в замене обработчика события WM_NCHITTEST, которое информирует об области, над которой в данный момент находится мышка. Обработчик этого сообщения также можно добавить через MFC ClassWizard. Предварительно на закладке ClassInfo для класса CAboutDlg нужно установить для Message Filter значение Window.

Переписываем функцию - обработчик следующим образом:

UINT CAboutDlg::OnNcHitTest(CPoint point)
{
    UINT ret = CDialog::OnNcHitTest(point);

    // если обработчик по умолчанию говорит нам что мышка
    // над клиентской областью окна,  заменяем возвращаемое
    // значение на HTCAPTION - мышка над заголовком окна,
    // а за заголовок перемещать окно можно!
    if (ret ==  HTCLIENT)
        return HTCAPTION;

    return ret;
}

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

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Если у вас есть вопрос по программированию, вы можете задать его одном из форумов на RSDN.

Это все на сегодня. Не забывайте заходить на RSDN. До встречи!

Алекс Jenter jenter@rsdn.ru
Красноярск, 2001. Рассылка является частью Проекта RSDN


No.42

Выпуск No. 42 от 29 апреля 2001 г.

Всем привет!

/ / / / СТАТЬЯ / / / / / / / / / / / / / / / / / / / / / /

Сериализация в MFC
Скорость, гибкость, типонезависимость

Автор: Джим Биверидж
Перевод: Олег Быков
Источник: www.ddj.com
Опубликовано: 17.04.2001
Версия текста: 1.0

Я следил за разработкой многих коммерческих программных продуктов от начального проектирования до выпуска рабочей версии, поэтому я скептически отношусь к концепции "компонент - черный ящик", так как следовать ей все труднее и труднее по мере развития и усложнения проекта. Когда я впервые познакомился с механизмом сериализации в библиотеке MFC [адекватного перевода английского слова "serialization" нет, поэтому здесь я использовал транслитерацию. В статье этот термин применяется для обозначения процесса сохранения/восстановления данных - прим.пер.], мне стало интересно, насколько этот механизм гибок и производителен для коммерческого применения. В процессе исследования я обнаружил, что, несмотря на некоторые ограничения, механизм сериализации в MFC основан на современной теории объектно-ориентированного проектирования и более того, этот механизм не привязан к какому-то определенному типу и допускает свое дальнейшее развитие.

Использовать MFC-сериализации несложно. Любой класс, производный от CObject, может переопределить функцию Serialize(), принимающую в качестве параметра объект класса CArchive. В этой функции Вы можете добавить свой код для сохранения и восстановления любых данных Вашего класса.

Сериализация данных производится с помощью операторов operator<< и operator>>, совсем как в случае с классом iostream. Разница в том, что CArchive подразумевает только двоичный формат данных. Подобно iostream, в CArchive реализованы операторы для чтения и записи фундаментальных типов данных, таких как long и char. Отсутствие типа данных int упрощает переносимость между 16- и 32-битными платформами. Встроенные операторы также реализуют перестановку байтов для типов, которые это поддерживают. (За дополнительной информацией о совместимости между платформами с прямой и обратной записью байтов [Little-Endian и Big-Endian] обратитесь к книге "Endian-Neutral Software," by James R. Gillig, DDJ, October/November 1994).

Реализация сериализации в MFC выглядела очевидной и неинтересной до того момента, когда мне понадобилось создать несколько типов документов в одном приложении. Я заметил, что когда я открывал в программе файл, MFC корректно создавала объект документа нужного типа и вызывала соответствующую функцию Serialize(). Это происходило, несмотря на то, что я не написал ни строчки кода, чтобы помочь MFC в создании документов. По крайней мере, я так считал:

Проблемы, всюду проблемы

Чтобы создавать документ или любой другой вид объектов "на лету", MFC должна решить три проблемы:

Проблема 1. По мере необходимости должны создаваться объекты произвольных типов, но оператор new может работать только с явно указанным типом, поэтому для CObject нужно реализовать некое подобие "виртуальных конструкторов".

Проблема 2. Разработчики должны иметь возможность легко "обучать" CObject создавать новые типы классов. В идеале, это должно делаться в определении и/или реализации класса.

Проблема 3. Необходим механизм сопоставления для создания объекта нужного типа по информации, прочитанной из файла. Этот механизм не может быть жестко зашит в MFC, потому что разработчики все время добавляют новые типы данных.

Как будет продемонстрировано далее, MFC элегантно решает эти проблемы, используя реестр с автоматической регистрацией типов [здесь и далее под реестром понимается программная структура, а не реестр WIndows - прим.пер.] и реализуя виртуальные конструкторы на основе зарегистрированных типов. Идентификация типов во время исполнения (RTTI) библиотеки MFC - вот краеугольный камень этой архитектуры.

Реестр типов

Чтобы осуществить идентификацию объектов во время выполнения, MFC создает в приложении реестр классов, унаследованных от CObject. Этот реестр никак не связан с OLE-реестром, но их концепции схожи. Реестр типов представляет собой связанный список структур CRuntimeClass, в котором каждая структура описывает один класс-наследник CObject. На листинге 1 показано внутреннее устройство структуры CRuntimeClass.

Листинг 1

struct CRuntimeClass
{
// Attributes
    LPCSTR m_lpszClassName;
    int m_nObjectSize;
    UINT m_wSchema;             // номер схемы загруженного класса
    void (PASCAL* m_pfnConstruct)(void* p); // NULL => abstract class
    CRuntimeClass* m_pBaseClass;
// Operations
    CObject* CreateObject();
// Implementation
    BOOL ConstructObject(void* pThis);
    void Store(CArchive& ar);
    static CRuntimeClass* PASCAL Load(
                CArchive& ar, UINT* pwSchemaNum);
    // объекты CRuntimeClass, связанные в простой список
    CRuntimeClass* m_pNextClass;// список зарегистрированных классов
};

Весь фокус в том, что типы из этого реестра не прописаны ни в одной таблице. Первый ключ к разгадке этого феномена находится в начале файла SCRIBDOC.H из MFC-примера "Scribble". Начало объявления класса выглядит так, как показано в примере 1(a).

Пример 1: (a) Начало объявления класса; (b) после обработки препроцессором макрос DECLARE_DYNCREATE разворачивается в несколько новых членов класса.

(a)
class CScribDoc : public CDocument
{
protected: // создавать только при сериализации
    CScribDoc();
    DECLARE_DYNCREATE(CScribDoc)
 ...
};

(b)
protected:
    static CRuntimeClass* __stdcall _GetBaseClass();
public:
    static  CRuntimeClass classCScribDoc;

   virtual CRuntimeClass* GetRuntimeClass() const;
    staticvoid__stdcall Construct(void* p);

В документации сказано, что макрос DECLARE_DYNCREATE позволяет классам-наследникам CObject создаваться динамически во время выполнения. Хотя это определение абсолютно верно, то, что происходит внутри этого макроса, гораздо интереснее. После обработки препроцессором макрос DECLARE_DYNCREATE разворачивается в несколько новых членов класса, как показано в примере 1(b) (Все примеры взяты из MFC 3.1 и Visual C++ 2.1. Я выровнял код, сгенерированный препроцессором, для повышения читабельности).

Идентификация типов времени выполнения в MFC базируется на виртуальной функции GetRuntimeClass(). Информация о типе доступна для любого объекта - потомка CObject, который включает в себя макросы DECLARE_DYNAMIC, DECLARE_DYNCREATE, или DECLARE_SERIAL. Эта информация позволяет Вам определить, может ли объект быть приведен к типу унаследованного класса или принадлежат ли два объекта одному и тому же классу. Хотя Visual C++ и не поддерживает новый C++-оператор dynamic_cast [помним, что речь идет о Visual C++ 2.1 - прим.пер.], использование вышеописанной информации о типе времени выполнения даст тот же эффект.

Информация о типе времени выполнения объявляется при помощи статической переменной класса, в данном случае classCScribDoc. Это имя (без пробела внутри) создается в макросах DECLARE_xxx через оператор макро-конкатенации. Для доступа к этой переменной используются функции класса _GetBaseClass() и GetRuntimeClass(). GetRuntimeClass() является виртуальной, поэтому тип объекта может быть определен даже через указатель на CObject.

И наконец, статическая функция класса Construct() образует базис для использования реестра классов MFC как фабрики классов, которая может, когда требуется, создавать объекты произвольных типов. Для понимания работы Construct() необходимы дополнительные разьяснения.

Создание объекта

В книге Advanced C++: Programming Styles and Idioms (Addison-Wesley, 1992), Джеймс O. Коплин так описывает концепцию виртуального конструктора:

Виртуальный конструктор используется в случае, когда необходимо определять тип объекта из контекста, в котором конструируется этот объект.

В MFC контекстом является информация, прочитанная из упорядоченного (serialized) архива [под архивом в этой статье понимается хранилище данных - прим.пер.]. Однако, виртуальный конструктор - это лишь концепция; никакая конструкция языка не реализует ее напрямую. Оператор new требует явного указания типа. Виртуальные конструкторы могут быть реализованы, если ввести в каждый класс статическую функцию, которая будет вызывать new для этого класса. Эта статическая функция-член будет вызываться при создании объекта определенного типа.

В MFC эта функция-член называется Construct(). Она создается макросами IMPLEMENT_DYNCREATE или IMPLEMENT_SERIAL. Один из этих макросов обязательно должен появиться в .cpp-модуле ровно один раз для каждого класса с поддержкой динамического создания. В случае со Scribble, выражение IMPLEMENT_DYNCREATE (CScribDoc, CDocument) появляется почти в самом начале файла SCRIBDOC.CPP. Первым аргументом идет класс, а вторым - его класс-родитель. Листинг 2 показывает код, сгенерированный препроцессором.

Когда MFC нужен документ или любой другой объект класса, унаследованного от CObject, она вызывает функцию CreateObject() класса CRuntimeClass. CreateObject() выделяет память, используя размер, указанный в структуре CRuntimeClass, и после этого вызывает ConstructObject(). ConstructObject() проверяет, поддерживает ли класс динамическое конструирование и вызывает функцию Construct() создаваемого класса.

Хотя в исходных текстах не дается пояснений, ясно, что такая схема четко разделяет конструирование объекта и выделение памяти. Все это кажется лишним, но в определенных ситуациях без такой организации не обойтись. Например, чтобы при создании массива его элементы располагались в одном блоке памяти, эту память нужно выделять одним вызовом функции malloc(). Используя ConstructObject(), Вы можете вручную инициализировать каждый элемент. Такой механизм позволяет принимать на этапе выполнения решения, которые в C++ обычно принимаются на этапе компиляции.

В примере 2 показана функция Construct(). Синтаксис вызова new немного необычен. На самом деле вызывается функция CObject::operator new(size_t, void*). Помните, что размер структуры - это подразумеваемый аргумент при вызове new, однако его следует явно описать в определении оператора. Эта версия new в CObject ничего не делает, но вызов new дает побочным эффектом вызов конструктора для этого объекта. Память уже была выделена вызовом CreateObject с использованием информации о размере из CRuntimeClass.

Пример 2: Функция Construct().

void__stdcall CScribDoc::Construct
(void* p)
{
    new(p) CScribDoc;
}

Используя реестр классов CRuntimeClass и функцию-член Construct(), MFC удается находить и создавать объекты новых типов на лету, что решает Проблему 1. Потенциально серьезные последствия данной техники в том, что при этом не поддерживаются множественное наследование и виртуальные базовые классы (см. MFC Technical Note #16).

Регистрация типов

Проблема 2 состоит в том, что пользователи должны иметь возможность легко добавлять новые классы в реестр. Идея саморегистрирующихся типов - ключевая идея объектно-ориентированного проектирования. Если каждый тип сам регистрирует факт своего существования в реестре, вместо того, чтобы программист прописывал его в реестре заранее, тогда типы можно свободно добавлять и удалять из программы, не меняя структуры реестра.

Хоть это и неочевидно, именно макрос IMPLEMENT_DYNCREATE позволяет пользователям без проблем добавлять новые классы в реестр. После развертывания IMPLEMENT_DYNCREATE, как показано на листинге 2, статическая структура CRuntimeClass в CScribDoc инициализируется так, как показано в примере 3.

Листинг 2

void__stdcall CScribDoc::Construct(void* p)
{
    new(p) CScribDoc;
}
CRuntimeClass* __stdcall CScribDoc::_GetBaseClass()
{
    return (&CDocument::classCDocument);
}
CRuntimeClass CScribDoc::classCScribDoc = {
    "CScribDoc",
     sizeof(CScribDoc),
    0xFFFF,
    CScribDoc::Construct,
    &CScribDoc::_GetBaseClass, 0 };
staticconst AFX_CLASSINIT _init_CScribDoc(&CScribDoc::classCScribDoc);
CRuntimeClass* CScribDoc::GetRuntimeClass() const
{
    return &CScribDoc::classCScribDoc;
}

Пример 3: Инициализация статической структуры CRuntimeClass в CScribDoc.

CRuntimeClass CScribDoc::
classCScribDoc = {
    "CScribDoc",
     sizeof(CScribDoc),
    0xFFFF,
    CScribDoc::Construct,
    &CScribDoc::_GetBaseClass,
    0 };

Некоторые из элементов этой структуры мы уже рассматривали. В частности, выражение sizeof(CScribDoc) используется CreateObject для выделения нужного объема памяти; затем эта память инициализируется функцией, на которую указывает CScribDoc::Construct.

Следующая строка помещает эту информацию в реестр типов MFC: static const AFX_CLASSINIT _init_CScribDoc(&CScribDoc::classCScribDoc);. Это объявление конструирует объект типа AFX_CLASSINIT, используя конструктор, который принимает как аргумент CRuntimeClass. Из-за того, что этот объект находится в области видимости файла, он будет сконструирован до вызова функции main(). Конструктор AFX_CLASSINIT присоединяет переданную ему структуру типа CRuntimeClass к реестру типов MFC. У AFX_CLASSINIT нет своих данных, поэтому он не требует дополнительной памяти.

Такой механизм делает возможной регистрацию типов объектов на лету каждый раз, когда они линкуются с программой, решая, таким образом, Проблему 2.

Часто разработчики задаются вопросом - в чем разница между различными макросами DECLARE и IMPLEMENT? Все макросы DECLARE_DYNAMIC и IMPLEMENT_DYNAMIC определяют статическую структуру CRuntimeClass, подобно DYNCREATE, описанному ранее, за одним исключением - поле Construct в этой структуре установлено в NULL. DECLARE_DYNCREATE и IMPLEMENT_DYNCREATE передают в структуру адрес функции Construct() для динамического создания типа. DECLARE_SERIAL и IMPLEMENT_SERIAL основываются на макросах DYNCREATE, но заменяют поле со значением 0xFFFF на номер схемы этой структуры.

Макросы SERIAL также определяют для класса operator>>. Этот оператор требует особого подхода, так как ему передается указатель на класс, но ни один из экземпляров этого класса не будет существовать, пока экземпляр не будет загружен из файла. Без экземпляра класса, MFC не может получить доступ к информации о классе времени выполнения для проверки на то, что загружаемый объект является объектом того же класса (или класса-наследника), что и переданный указатель. Перегружая operator>>, MFC получает возможность передавать указатель на информацию о типе времени выпонения, чтобы механизм сериализации не зависел от типа (typesafe serialization).

Создание типов из файла

Третья проблема состоит в том, чтобы создать механизм сопоставления для создания типов по информации, прочитанной из файла. Учитывая то, что компилятор требует уникальности имен классов и то, что имя класса уже включено в структуру CRuntimeClass, имя класса является идеальным кандидатом на запись в файл и последующую идентификацию класса.

Итак, при сохранении объекта в архиве можно записать туда имя класса и его данные. MFC так и делает, плюс проводит дополнительную работу для каждого сериализуемого класса. Имя класса берется из структуры CRuntimeClass, которая возвращается виртуальной функцией объекта. Определение типа производится динамически во время выполнения, поэтому структура типа Tiger будет корректно записана даже в случае, если MFC передается указатель на ее базовый класс типа Animal. Эта типонезависимость очень важна. Любая функция может без опасений сохранить объект в архиве, даже если точный тип объекта неизвестен.

То же касается и восстановления объекта из архива. Возвращаясь к примеру в начале статьи, несколько простых выражений из примера 4 заставляют MFC успешно загружать корректный тип документа из файла.

Пример 4: Загрузка правильного типа документа из файла.

CDocument* pDoc;
CArchive& ar;
 ...
ar >> pDoc;

В реализации operator>> MFC загружает из файла имя класса, и ищет это имя в списке типов. Если этот тип присутствует в реестре и был описан либо как DECLARE_DYNCREATE, либо как DECLARE_SERIAL, MFC может сконструировать требуемый объект. Непосредственная загрузка данных этого объекта возлагается на сам объект вызовом его виртуальной функции Serialize(), что решает Проблему 3.

Тип создаваемого объекта не привязан к типу запрошенного объекта. Если класс-потомок загружается через указатель на класс-предок, как в примере с CDocument, все равно создастся корректный класс-потомок. Это единственный способ корректно задать указатель vtbl так, чтобы он указывал на виртуальные функции объекта. Если объект в архиве не является "родственником" запрошенного объекта, MFC взведет исключение.

Этот механизм таит в себе две потенциальные ловушки для разработчиков. Во-первых, переименование сериализуемой структуры или класса сделает невозможным его восстановление из старых архивов. Во-вторых, MFC не записывает в архив длину каждого объекта. Поэтому, если MFC не сможет загрузить объект, она не сможет пропустить его и загрузить оставшуюся часть архива.

Оптимизация архивов

Когда я впервые просматривал этот код, мне виделись распухшие файлы с объектами и большие задержки при постоянном просмотре связанного списка. Но архивы MFC остаются маленькими, а скорость выполнения высокой благодаря хэш-идентификаторам.

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

Такое поведение также означает, что множественные ссылки на один и тот же объект обрабатываются корректно. Если объекты A и B при создании архива содержат указатели на один объект C, они оба будут указывать на один объект C после восстановления их из архива. MFC также корректно восстановит циклические меж-объектные ссылки.

В результате все работает гораздо быстрее, чем я ожидал. На 486/66, MFC смогла сохранить и восстановить архив размером более мегабайта с 10000 экземплярами CArray<DWORD,DWORD> менее, чем за 2 секунды.

Есть одно важное ограничение - хэш-таблица не может содержать больше 32766 классов и объектов в контексте одного архива. Это число включает в себя только классы, унаследованные от CObject и сериализуемые оператором operator<<, и не включает фундаментальные типы, например, short и long, CString и CPoint. (за дополнительной информацией о конструировании архивов обратитесь к MFC Technical Note 2: Persistent Object Data Format).

Версии схем сериализации

Одной из самых слабодокументированных особенностей 32-битной MFC 3.2 является поддержка версий схем сериализации (versionable schemas), когда MFC позволяет функции Serialize() обрабатывать разные версии одного класса вместо того, чтобы взводить исключение. Эта особенность очень важна для эволюционирующего проекта. И хотя я опишу, как реализовать поддержку версий, в Visual C++ 2.x этот механизм содержит ошибку и рушит программу при выполнении. Рекомендую написать в Microsoft, как сильно Вы ждете исправления этой ошибки [можно еще воспользоваться компилятором поновее - прим.пер.].

В MFC с каждой структурой, использующей DECLARE_SERIAL и IMPLEMENT_SERIAL, ассоциирован номер версии. Обычно этот номер установлен в 1, как показано в большинстве MFC-примеров; например, так - IMPLEMENT_SERIAL(CStroke, CObject, 1).

У каждой структуры или класса есть свой номер версии, который может изменяться независимо от остальных. MFC автоматически записывает в архив номер версии после идентификатора класса. До версии 3.0 в MFC не было полной поддержки механизма версий сериализации, поэтому старые версии MFC взводили исключение, когда номер схемы объекта в файле не совпадал с текущим номером схемы. Это рушило поддержку множественных схем.

В MFC 3.0 и более поздних версиях, такое поведение сохранилось, но его можно изменить. Если объединить оператором OR третий параметр макроса IMPLEMENT_SERIAL с константой VERSIONABLE_SCHEMA, MFC позволит работать с версией схемы сериализации в Вашей функции Serialize(). Например, чтобы установить номер версии документа в 3, используйте выражение DECLARE_SERIAL(CScribDoc, CDocument, VERSIONABLE_SCHEMA|3).

Чтобы использовать эту возможность, при загрузке данных из архива класс должен вызвать функцию GetObjectSchema() в своей функции Serialize(), как показано на листинге 3.

Листинг 3

class CSmallObject : public CObject {
   DECLARE_SERIAL(CSmallObject);
   DWORD m_value;   // было unsigned short в версии 1
};
IMPLEMENT_SERIAL(CSmallObject, CObject, VERSIONABLE_SCHEMA| 2);
CSmallObject::Serialize(CArchive& ar)
{
   if (ar.IsStoring()) {
      ...
   }
   else {
      DWORD nVersion = ar.GetObjectSchema();
      switch (nVersion) {
         case -1:
            // -1 показывает, что структура была создана с DYNCREATE,
            // а не с SERIAL. Появление этого значения говорит об ошибке.
            break;
         case1:    // Эта версия использовала unsigned short
         unsignedshort oldval;
            ar >> oldval;
            m_value = oldval;
            break;
         case2;
            // Текущая версия использует DWORD
            ar >> m_value;
            break;
         default:
            // несуществующее значение - скорее всего, данные испорчены.
            break;
      }
   }
}
Заключение

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

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

Как добавить всплывающие подсказки для элементов управления диалога?

Авторы: Игорь Вартанов
Александр Шаргин

Версия текста: 1.1

Демонстрационный проект ToolTip

Демонстрационный проект MFCTips

Win32 API

Ограничимся простейшим (но не самым бесполезным!) набором функций, которые мы хотим получить от подсказок. Чаще всего необходимо добавить появление подсказки для определенных областей окна (будь то контролы или отведенные для этой цели прямоугольники), кроме того необходимо иметь возможность изменять текст подсказок и при определенных обстоятельствах блокировать их вывод. Разобравшись с указанными вопросами, достаточно легко расширить функциональность и вариативность их поведения.

Нам понадобится следующий набор функций:

HWND APIENTRY CreateToolTip(HWND hWndParent);
void APIENTRY FillInToolInfo(TOOLINFO* ti, HWND hWnd, UINT nIDTool = 0);
BOOL APIENTRY AddTool(HWND hTip, HWND hWnd, RECT* pr = NULL, UINT nIDTool = 0, LPCTSTR szText = NULL);
void APIENTRY UpdateTipText(HWND hTip, HWND hWnd, UINT nIDTool = 0, LPCTSTR lpszText = NULL);
void APIENTRY GetTipText(HWND hTip, HWND hWnd, UINT nIDTool, LPSTR szText);
void APIENTRY EnableToolTip(HWND hTip, BOOL activate);

Вот пример их реализации (демонстрация применения в тестовом проекте Tooltip).

Название CreateToolTip( ) достаточно прозрачно для понимания того, что же делает эта функция. В ней происходит инициализация системной библиотеки управляющих элементов и создание собственно контрола ToolTip. Обычно родителем выступает окно диалога (либо главное окно приложения).

//-------------------------------------------------------------
HWND APIENTRY CreateToolTip(HWND hWndParent)
{
    InitCommonControls();
    HWND hTip = CreateWindowEx(
        0,TOOLTIPS_CLASS,
        0,0,0,0,0,0,
        hWndParent,0,0,0);
    return hTip;
}

Функция FillInToolInfo( ) играет вспомогательную роль для выполнения рутинных операций со структурой TOOLINFO. Логика поведения функции предусматривает использование в качестве уникального идентификатора области вывода подсказки (которая в MSDN носит название tool) хэндла окна - носителя подсказки в случае, если в нее передан нулевой идентификатор nIDTool. В случае ненулевого значения nIDTool программист сам должен обеспечить уникальность передаваемых значений.

//-------------------------------------------------------------
void APIENTRY FillInToolInfo(TOOLINFO* ti, HWND hWnd, UINT nIDTool)
{
    ZeroMemory(ti,sizeof(TOOLINFO));
    ti->cbSize = sizeof(TOOLINFO);
    if(!nIDTool)
    {
        ti->hwnd   = GetParent(hWnd);
        ti->uFlags = TTF_IDISHWND;
        ti->uId    = (UINT)hWnd;
    }
    else
    {
        ti->hwnd   = hWnd;
        ti->uFlags = 0;
        ti->uId    = nIDTool;
    }
}

Добавить новую область подсказки можно функцией AddTool( ). Данная реализация AddTool( ) предусматривает, что контрол hTip сам обеспечит себе получение системных сообщений о передвижении мыши от окон - носителей подсказки. Для этого при создании области выставляется флаг TTF_SUBCLASS. В этом случае совершенно отпадает необходимость в использованиии механизма TTM_RELAYEVENT. Флаг TTF_TRANSPARENT опционален и означает, что выводимые окна подсказки будут прозрачны для мышиных сообщений.

Существует возможность отложить установку текста подсказки на более позднее время. Для этого просто передается NULL-указатель в качестве указателя на текст подсказки. Вместо NULL в ToolTip контрол будет передано значение LPSTR_TEXTCALLBACK, говорящее контролу, что при необходимости он сможет получить текст подсказки посредством механизма нотификации (через WM_NOTIFY) посылкой TTN_GETDISPINFO (эквивалентное ему TTN_NEEDTEXT).

Кроме того AddTool( ) предусматривает возможность ограничения чувствительной области окна (не только окна диалога, но и окна любого контрола) явно задаваемым прямоугольником (если указатель на него равен NULL, будет использована вся клиентская область окна). Однако, при добавлении области подсказки имеет значение способ идентификации области подсказки - если она основана на использовании хэндла окна в качестве идентификатора (установлен флаг TTF_IDISHWND), то чувствительной областью становится вся клиентская область окна - носителя, а координаты прямоугольника (даже если они указаны явно) будут игнорироваться. Как видно из реализации функции FillInToolInfo( ), это будет происходить для случаев, когда nIDTool равен нулю.

//-------------------------------------------------------------
BOOL APIENTRY AddTool(HWND hTip, HWND hWnd, RECT* pr, UINT nIDTool, LPCTSTR szText)
{
    TOOLINFO ti;
    RECT     r = {0,0,0,0};

    FillInToolInfo(&ti, hWnd, nIDTool);
    ti.hinst  = (HINSTANCE)GetModuleHandle(NULL);
    ti.uFlags |= TTF_SUBCLASS | TTF_TRANSPARENT;
    ti.lpszText = LPSTR( szText ? szText : LPSTR_TEXTCALLBACK );
    if(!(ti.uFlags & TTF_IDISHWND))
    {
        if(!pr)
        {
            pr = &r;
            GetClientRect(hWnd, pr);
        }
        memcpy(&ti.rect, pr, sizeof(RECT));
    }
    BOOL res = SendMessage(hTip, TTM_ADDTOOL, 0, (LPARAM)&ti);
    return res;
}

После того, как область зарегистрирована, можно управлять ее текстом посредством UpdateTipText( ). Можно заметить, что в ней может быть использован тот же механизм обратного вызова текста подсказки, что и в AddTool( ). Т.е. в том случае, если указатель lpszText будет установлен в NULL, то будет задействован механизм обратного вызова текста подсказки. А как же поступить в случае, если нужно просто прекратить вывод какой-либо одной подсказки, если установка lpszText в NULL задействует альтернативный способ? В этом случае нужно, чтобы lpszText указывал на пустую строку "".

//-------------------------------------------------------------
void APIENTRY UpdateTipText(HWND hTip, HWND hWnd, UINT nIDTool, LPCTSTR lpszText)
{
    TOOLINFO ti;
    FillInToolInfo(&ti, hWnd, nIDTool);
    ti.lpszText = LPSTR( lpszText ? lpszText : LPSTR_TEXTCALLBACK );
    SendMessage(hTip, TTM_UPDATETIPTEXT, 0, (LPARAM)&ti);
}

Получить текст конкретной подсказки можно посредством GetTipText( ).

//-------------------------------------------------------------
void APIENTRY GetTipText(HWND hTip, HWND hWnd, UINT nIDTool, LPSTR szText)
{
    TOOLINFO ti;
    if(!szText)
        return;
    *szText = 0;
    FillInToolInfo(&ti, hWnd, nIDTool);
    ti.lpszText = szText;
    SendMessage(hTip, TTM_GETTEXT, 0, (LPARAM)&ti);
}

Включить/выключить вывод всех подсказок, зарегистрированных данным tooltip-контролом, можно функцией EnableToolTip( ).

//-------------------------------------------------------------
void APIENTRY EnableToolTip(HWND hTip, BOOL activate)
{
    SendMessage(hTip, TTM_ACTIVATE, activate, 0);
}
ПРИМЕЧАНИЕ
Необходимо отметить, что в данной реализации способа работы с областями подсказки имеется одно ограничение - если программист явным образом задает идентификаторы областей подсказки (флаг TTF_IDISHWND в этом случае не установлен), то механизм обратного вызова текста подсказки не работает, поскольку нотификационные сообщения обратного вызова приходят не диалогу, а окну-носителю области подсказки, которое не умеет их обрабатывать (в данной реализации).
MFC

В MFC для работы с всплывающими подсказками предназначен класс CToolTipCtrl. Рассмотрим, как им пользоваться.

Первым делом необходимо добавить объект класса CToolTipCtrl в класс диалогового окна, которое вы хотите снабдить всплывающими подсказками. Тем самым мы гарантируем, что этот объект будет существовать ровно столько, сколько сам диалог. Например:

class CMFCTipsDlg : public CDialog
{
...
protected:
    CToolTipCtrl m_tt;
...
};

Хотя большую часть времени всплывающая подсказка не видна на экране, это обыкновенное окно, и прежде чем работать с ним, его необходимо создать и связать с уже имеющимся у нас объектом m_tt. Для этого используется функция CToolTipCtrl::Create, которая получает указатель на объект родительского окна и стиль подсказки. Обычно её вызывают из обработчика WM_INITDIALOG родительского окна подсказки, например:

BOOL CMFCTipsDlg::OnInitDialog()
{
...

    m_tt.Create(this);
...
};

Следующая наша задача - сообщить всплывающей подсказке, над какими контролами она должна появляться и какой текст при этом выдавать. Для этого нужно зарегистрировать каждый контрол в подсказке. Это выполняется с помощью функции CToolTipCtrl::AddTool.

BOOL AddTool(
    CWnd* pWnd,
    LPCTSTR lpszText = LPSTR_TEXTCALLBACK,
    LPCRECT lpRectTool = NULL,
    UINT nIDTool = 0
);

Параметры pWnd и lpRectTool задают окно и прямоугольную область внутри этого окна, над которой будет появляться подсказка, а в nIDTool записывается уникальный идентификатор этой области. Если задать lpRectTool равным NULL, создаётся область, занимающая окно целиком. Именно это нам и требуется, поскольку мы хотим добавить подсказки для контролов в диалоге. В этом случае nIDTool должен быть равен нулю (значение по умолчанию). Параметр lpszText содержит указатель на текст подсказки. Если передать вместо текста значение LPSTR_TEXTCALLBACK, подсказка будет запрашивать его непосредственно перед отображением, посылая окну, содержащему контрол (или прямоугольную область), сообщение TTN_GETDISPINFO. О том, как обрабатывать это сообщение, мы поговорим немного позже.

Обычно подсказки для контролов также назначают в обработчике WM_INITDIALOG. Поступим так и мы. Например:

BOOL CMFCTipsDlg::OnInitDialog()
{
...

    staticint ID[] =
    {
        IDC_PICTURE,
        IDC_TEXT,
        IDC_EDIT,
        IDC_COMBO,
        IDC_RADIO1,
        IDC_RADIO2,
        IDC_RADIO3,
        IDC_CHECK,
        IDC_LIST,
        IDC_TREE,
        IDOK,
        IDCANCEL
    };

    staticconstchar *szTipText[] =
    {
        "Picture",
        "Text",
        "Edit",
        "Combo box",
        "Radio button 1",
        "Radio button 2",
        LPSTR_TEXTCALLBACK,
        "Check box",
        "List view",
        "Tree view",
        "OK",
        "Cancel"
    };

    for(int i=0; i<sizeof(ID)sizeof(int); i++)
        m_tt.AddTool(GetDlgItem(ID[i]), szTipText[i]);
...
}

Следующее, что нам нужно сделать - направить в подсказку все мышиные сообщения, которые получает диалог. Иначе подсказка не сможет определить, что пользователь задержал курсор над одной из зарегистрированных областей. Перенаправление сообщений в подсказку осуществляется с помощью функции CToolTipCtrl::RelayEvent. Проще всего вызывать её из функции CWnd::PreTranslateMessage, так как в неё попадают все сообщения, адресованные диалогу или одному из его дочерних окон. При этом можно сделать небольшую оптимизацию, передавая в подсказку не все подряд сообщения, а только сообщения, связанные с мышью. Выглядит это так.

BOOL CMFCTipsDlg::PreTranslateMessage(MSG* pMsg)
{
    if(pMsg->message >= WM_MOUSEFIRST && pMsg->message <= WM_MOUSELAST)
        m_tt.RelayEvent(pMsg);

    return CDialog::PreTranslateMessage(pMsg);
}

Вот и всё. Проделанных действий достаточно, чтобы подсказки начали появляться. Осталось рассмотреть, как обрабатывать сообщение TTN_GETSIDPINFO. Как уже говорилось, оно посылается, перед отображением подсказки, в случае если текст подсказки не был задан заранее. Сообщение TTN_GETDISPINFO обрабатывается по обычной схеме (при помощи макроса ON_NOTIFY или ON_NOTIFY_RANGE). Если вы решите использовать макрос ON_NOTIFY, вам понадобится значение идентификатора подсказки. В текущей версии MFC этот идентификатор равен NULL, но учтите, что это значение нигде не документировано. Используя ON_NOTIFY_RANGE, вы не попадёте в зависимость от недокументированных параметров. Например:

class CMFCTipsDlg : public CDialog
{
...
    afx_msg void OnGetDispInfo(UINT id, NMTTDISPINFO *pNMHDR, LRESULT *pResult);
...
};

BEGIN_MESSAGE_MAP(CMFCTipsDlg, CDialog)
...
    ON_NOTIFY_RANGE(TTN_GETDISPINFO, 0, 0xFFFFFFFF, OnGetDispInfo)
...
END_MESSAGE_MAP()

void CMFCTipsDlg::OnGetDispInfo(UINT id, NMTTDISPINFO *pNMHDR, LRESULT *pResult)
{
    pNMHDR->lpszText = "Radio button 3";
    *pResult = 0;
}

В этом фрагменте мы просто возвращаем предопределённую строку "Radio button 3", так как ранее мы не задали текст подсказки всего для одного контрола. Если же таких контролов несколько, вам придётся сначала проанализировать значения hwnd, uId и rect структуры NMTTDISPINFO, а затем вернуть соответствующую им строку.

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Это все на сегодня. До следующей недели!

Алекс Jenter jenter@rsdn.ru
Красноярск, 2001. Рассылка является частью проекта RSDN.


No.43

Выпуск No. 43 от 6 мая 2001 г.

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

Технические работы на сайте закончились и теперь форум и поиск снова работают нормально, причем форум очень сильно изменился соответственно вашим пожеланиям! Можете зайти и посмотреть сами на RSDN.RU.

/ / / / СТАТЬЯ / / / / / / / / / / / / / / / / / / / / / /

Свойства в С++

Автор: Денис Майдыковский

Версия текста: 1.1

Эта статья написана по материалам дискуссии в конференции RU.VISUAL.CPP сети FIDO. Основой примера шаблона свойства послужило письмо от "Vyacheslav V. Lanovets" от 6 декабря 2000г.

Развитие современных систем программирования в направлении объектно-ориентированного и компонентно-ориентированного, а также визуального программирования привело к тому, что многие распространённые и хорошо известные алгоритмические языки перестали соответствовать потребностям современного рынка средств разработки. Одним из таких "нововведений" стали свойства объекта. В общем случае, свойство это пара функций, одна из которых отвечает за установку некоторого значения, а другая за его считывание. Такое решение позволяет, во-первых, обеспечить инкапсуляцию данных и защиту их от неправомерного доступа, а во-вторых, обеспечить целостность данных. Необходимость использования свойств возникает тогда, когда при изменении некоторого параметра требуется произвести ещё некоторые действия.

В тех языках программирования, синтаксис которых находится под контролем создателей (таких как "Visual Basic" или "Delphi") концепция свойств реализована на уровне синтаксиса. В частности, обращение к свойствам объекта производится оператором присваивания, как при обращении к переменной-члену класса в C++. Однако, не стоит обольщаться по поводу простоты синтаксиса. Не стоит забывать, что в простейшем выражении типа

theObject.theProperty = theValue

производится неявный вызов функции.

К сожалению, строгий и стандартизированный язык C++ не позволяет добавлять в его синтаксис столь революционные новации. Какие же возможности предоставляет разработчику C++?

Наиболее простой и самый распространённый способ обеспечения инкапсуляции в C++ заключается в написании пары функций типа get_Value() и put_Value() для каждого параметра. Заметим, что именно так реализованы свойства в технологии Automation. При использовании этого способа можно написать примерно такой класс:

class CValue
{
private:
    int m_value;
public:
    int get_Value()
    {
        return m_value;    // Или более сложная логика
    }
    void put_Value(int value)
    {
        m_value = value;     // Или более сложная логика
    }
};

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

Хорошо это или плохо, но современные средства разработки "приучили" многих к использованию свойств в операторах присваивания и вообще обращению с ними, как с переменными-членами. Учитывая это, разработчики Microsoft Visual C++ добавили в синтаксис языка несколько "Microsoft Specific" конструкций. В частности, модификатор __declspec получил дополнительный параметр "property". Теперь в классе можно объявить "виртуальную" переменную и связать её с соответствующими функциями. Теперь класс может выглядеть примерно так:

class CValue
{
private:
    int m_value;
public:
    __declspec(property(get=get_Value, put=put_Value)) int Value;
    int get_Value()
    {
        return m_value;     // Или более сложная логика
    }
    void put_Value(int value)
    {
        m_value = value;     // Или более сложная логика
    }
};

Строчка сразу за "public:" объявляет "виртуальную" переменную типа int, при обращении к которой фактически будут вызваться функции. С этим классом можно будет работать примерно так:

CValue val;
val.Value = 50;        // На самом деле вызов put_Value()
int z = val.Value;    // На самом деле вызов get_Value()

Чем не "настоящие" свойства? Однако следует заметить, что модификатор __declspec(property) был введён не для повседневного использования, а для встроенной в компилятор поддержки технологии СОМ. Дело в том, что директива импорта библиотеки типа (что бы знать, что это такое, читайте книжки по COM) #import заставляет компилятор VC автоматически генерировать вспомогательные классы-обёртки для объектов СОМ. Вот в этих "автоматических" классах модификатор __declspec(property) используется достаточно широко для максимальной приближенности к синтаксису Visual Basic'а. Приближенность к синтаксису VB достигает такой степени, что свойства сделаны индексными. Для этого, достаточно поставить квадратные скобки после объявления "виртуальной переменной":

__declspec(property(get=get_Value, put=put_Value)) int Value [];

После такого объявления свойство "Value" может принимать один или несколько параметров-индексов, передаваемых в квадратных скобках. Так, например, вызов

Val.Value[f]["two"] = 10;

Будет преобразован в вызов функции

Val.put_Value(f, "two", 10);

Главным недостатком описанного выше способа использования свойств в C++ является его зависимость от компилятора, пресловутая "Microsoft Specific". Впрочем, другой, не менее известный компилятор "Borland C++ Builder" реализует концепцию свойств далёким от стандарта способом. В любом случае часто требуется (или хочется) достичь независимости от компилятора и соответствия кода программы стандарту C++. Что же делать? Оказывается язык C++ позволяет реализовать концепцию свойств в стиле Visual Basic'а. Для этого необходимо воспользоваться шаблонами и перекрыть операторы присваивания и приведения типа. Но для начала необходимо провести некоторую подготовительную работу:

// Базовый класс шаблона свойства.
template <typename proptype, typename propowner> class property
{
protected:
    typedef proptype (propowner::*getter)();
    typedefvoid (propowner::*setter)(proptype);
    propowner * m_owner;
    getter m_getter;
    setter m_setter;
public:
    // Оператор приведения типа. Реализует свойство для чтения.
    operator proptype()
    {
        // Здесь может быть проверка "m_owner" и "m_getter" на NULL
        return (m_owner->*m_getter)();
    }
    // Оператор присваивания. Реализует свойство для записи.
    voidoperator =(proptype data)
    {
        // Здесь может быть проверка "m_owner" и "m_setter" на NULL
        (m_owner->*m_setter)(data);
    }
    // Конструктор по умолчанию.
    property() :
        m_owner(NULL),
        m_getter(NULL),
        m_setter(NULL)
    {
    }
    //Конструктор инициализации.
    property(propowner * const owner, getter getmethod, setter setmethod) :
        m_owner(owner),
        m_getter(getmethod),
        m_setter(setmethod)
    {
    }
    // Инициализация
    void init(propowner * const owner, getter getmethod, setter setmethod)
    {
        m_owner = owner;
        m_getter = getmethod;
        m_setter = setmethod;
    }
};

Теперь класс, реализующий свойство можно написать так:

class CValue
{
private:
    int m_value;
    int get_Value()
    {
        return m_value;    // Или более сложная логика
    }
    void put_Value(int value)
    {
        m_value = value;     // Или более сложная логика
    }
public:
    property <int, CValue> Value;
    CValue()
    {
        Value.init(this, get_Value, put_Value);
    }
};

А вот код, использующий этот класс:

CValue val;

/*
Здесь вызывается оператор присваивания переменной-члена val.Value,
и, следовательно, функция val.put_Value()
*/
val.Value = 50;

/*
Здесь вызывается оператор приведения типа переменной-члена val.Value,
и, следовательно, функция val.get_Value()
*/
int z = val.Value;

Как можно видеть, получились "настоящие" свойства средствами только стандартного синтаксиса С++. Однако, описанный метод не лишен недостатков:

  • При каждом обращении к "свойству" происходит два вызова функции.
  • Использование таких "свойств" требует дополнительных затрат памяти из-за того, что на каждое "свойство" требуется 3 дополнительных указателя, что составляет 12 байт накладных расходов.
  • Использование шаблонов приводит к увеличению размеров исполняемого кода, поскольку компилятор будет генерировать отдельный класс для каждой пары "proptype" и "propowner".
  • Для каждого "свойства" необходимо не забыть произвести инициализацию в конструкторе класса-владельца.

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

Как научить программу реагировать на изменение содержимого буфера обмена?

Автор: Александр Шаргин

Версия текста: 1.0

Программа-пример CbView

Программа-пример MfcCbView

В Windows существует понятие наблюдателя за буфером обмена (clipboard viewer), которым может стать любое окно. Наблюдатель получает от системы уведомления об изменении содержимого буфера обмена в виде сообщения WM_DRAWCLIPBOARD. Соответственно, в ответ на это сообщение программа может загрузить содержимое буфера обмена и выполнить с ним нужные операции (типичный пример - отобразить содержимое буфера обмена в окне).

Интересен способ взаимодействия системы с несколькими наблюдателями за буфером обмена. Дело в том, что с точки зрения Windows наблюдатель всегда один (он называется текущим), и только ему посылаются уведомления. Передача этих уведомлений дальше по цепочке наблюдаетей - задача приложения. Для этого каждая программа, регистрирующая наблюдателя за буфером обмена, получает и сохраняет в переменной HWND предыдущего наблюдателя, а затем передаёт ему сообщения с помощью одной из функций SendMessage, PostMessage и т. п. "Недобросовестная" программа, которая не передаёт уведомления дальше по цепочке, может нарушить работу других приложений и даже других экземпляров самой себя, поэтому писать такие программы настоятельно не рекомендуется.

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

BOOL CALLBACK DlgProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
    ...
    static HWND hNextViewer;
    ...
    hNextViewer = SetClipboardViewer(hDlg);
    ...
}

Следующий шаг - научить программу реагировать на сообщение WM_DRAWCLIPBOARD. Это очень простое сообщение, никак не использующее параметры wParam и lParam. Как я уже говорил, программа обязана передать это сообщение дальше по цепочке наблюдателей. Выглядит это так.

case WM_DRAWCLIPBOARD:
    // Работаем с буфером обменаif(IsWindow(hNextViewer))
        PostMessage(hNextViewer, msg, wParam, lParam);
ПРИМЕЧАНИЕ
В общем случае весьма опасно использовать SendMessage для отправки сообщений чужим окнам. Если приложение, которому принадлежит окно, занято выполнением длительной операции или же просто "зависло" в бесконечном цикле, то в ожидании возврата из SendMessage наше приложение зависнет тоже. Вот почему лучше использовать PostMessage, как это сделано в примере выше, или воспользоваться функциями типа SendMessageTimeout.

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

ChangeClipboardChain(hDlg, hNextViewer);

Функция ChangeClipboardChain посылает текущему наблюдателю сообщение WM_CHANGECBCHAIN, передавая полученные хэндлы через параметры wParam и lParam. Текущий наблюдатель сравнивает wParam с хэндлом следующего наблюдателя, который он хранит в переменной. Если обнаружено совпадение, то он просто сохраняет в качестве HWND следующего наблюдателя значение lParam, тем самым удаляя окно нашего приложения из списка. В противном случае он должен передать WM_CHANGECBCHAIN дальше по цепочке. Вот как выглядит типичный обработчик сообщения WM_CHANGECBCHAIN:

case WM_CHANGECBCHAIN:
        if(hNextViewer == (HWND)wParam)
            hNextViewer = (HWND)lParam;
        elseif(IsWindow(hNextViewer))
            PostMessage(hNextViewer, msg, wParam, lParam);

Пример CbView иллюстрирует все принципы, которые мы только что рассмотрели. Программа CbView добавляет своё окно в цепочку наблюдателей за буфером обмена, когда пользователь устанавливает галочку "Spy clipboard". В ответ на WM_DRAWCLIPBOARD она проверяет содержимое буфера обмена, и если это текст (формат CF_TEXT), загружает его в RichEdit.

В MFC наблюдение за буфером обмена осуществляется по тому же самому принципу. В ней предусмотрены макросы ON_WM_DRAWCLIPBOARD и ON_WM_CHANGECBCHAIN для добавления соответствующих обработчиков в карту сообщений, а также функции SetClipboardViewer и ChangeClipboardChain класса CWnd, соответствующие одноимённым функциям из Win32 API. Программа-пример MfcCbView демонстрирует создание наблюдателя за буфером обмена с использованием MFC.

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Все на сегодня. До следующих встреч!

Алекс Jenter jenter@rsdn.ru
Красноярск, 2001. Рассылка является частью проекта RSDN.


No.44

Выпуск No. 44 от 13 мая 2001 г.

Добрый день!

/ / / / ОБРАТНАЯ СВЯЗЬ / / / / / / / / / / / / / / / / / / / / / /

В статье "Свойства в С++" в N43 был приведен пример "свойства", который не мог оставить меня равнодушным, как программиста, использующего язык C++ не один год.

Приведенный в статье пример, является забавной комбинацией COM и непреодолимым желанием автора сделать "как в бейсике". Кстати СОМ, берет начало с OLE и ActiveX, который создавался так, чтобы программистам на VB было как можно проще его использовать. А что хорошо для VB-программистов, то одна головная боль для программистов на С++. Отсюда и возникли добавляемые автоматически префиксы get_ и put_. Первый вариант класса CValue (с использованием __declspec) верен, и его, с небольшими изменениями, можно использовать в качестве COM интерфейса. Но для внутреннего использования (т.е. для использования только в С++) он и все последующие мало пригодны (минусы уже были перечислены).

Поэтому, в качестве опровержения некоторых утверждений, приведенных в той статье, предлагаю вашему вниманию свою статью под названием "Эффективное использование С++. Создание классов-оберток для стандартных типов данных".

Я не буду против опубликовании вами этой статьи.

С уважением, Илья Жарков.

/ / / / СТАТЬЯ / / / / / / / / / / / / / / / / / / / / / /

Эффективное использование С++.
Создание классов-оберток для стандартных типов данных

Автор: Илья Жарков

Большое распространение технологии COM и повальное увлечение всех начинающих программистов языками программирования высокого уровня (я имею ввиду Visual Basic & Delphi), приводит к тому, что в массовом сознании закрепляется твердое убеждение, что те средства, которые используются в данных технологиях и языках, является единственно верными и правильными. А что происходит, когда программист "взрослеет"? Он, в погоне за новыми возможностями, устремляется к другим языкам, например к C++. Но тут оказывается, что в этом языке нет привычных ему средств или их реализация не лежит на поверхности. Как всегда в программировании нет времени на детальное изучение возможностей языка (печально, если оно так и не появляется) - программу надо сдать завтра в 8 утра и не часом позже. Вот так и начинается повторное изобретение велосипеда.

Данная статья, я надеюсь, будет полезна программистам, начинающим изучать язык программирования С++, а также тем, кто хочет научиться использовать его возможности наиболее эффективным образом. Тут вы сможете прочитать о создании специальных классов, упрощающих использование стандартных типов данных и называемых "классами-обертками". Такие "классы-обертки" работают подобно нетипизированным переменным языка Visual Basic - производят нужные преобразования из одного типа в другой, а также хранят в себе несколько переменных разного типа.

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

struct Value {
int nVal;
char *str;
};

А использовалось следующим образом:

void init(Value* val)
{
 val.nVal=10;
 val.str=(char*)malloc(50);
}

То же самое, конечно, можно написать и на C++, но он предоставляет гораздо более мощное средство под названием класс. Этот пример можно переписать так (забегая немного вперед, скажу, что на практике чаще всего используются классы, подобные этому):

class CValue
{
 int   nVal;
 char* str;
public:
 CValue() { nVal=0; str=NULL; }
 ~CValue() { delete[] str; }
 CValue& Val(int val) { nVal=val; return *this; }
 CValue& Str(const char* string)
 {
  delete[] str;
  str=new char[strlen(string)+1];
  strcpy(str, string);
  return *this;
 }
 int Val() const { return nVal; }
 char* Str() const { return str; }
};

Вы спросите, что нам дает такое, казалось бы, громоздкое повторение предыдущей маленькой структуры. В первую очередь, контроль за значениями, хранящимися в переменных. Мы можем, например, ограничить диапазон переменной nVal значениями от 3 до 11, включив соответствующую проверку в функцию CValue& Val(int val). Благодаря спецификаторам доступа public и private (используемый неявно в начале класса) исключается несанкционированный доступ к переменным класса. Но не менее важно и то, что их значения не примут случайное значение (благодаря конструктору) и не произойдет утечки памяти (благодаря деструктору). Кроме этого очень сильно упрощается использование такой структуры.

CValue value;
// так происходит инициализация
value.Val(10).Str("string");
// так значения используются
int n=value.Val();
char *str=value.Str();

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

Теперь воспользуемся таким средством C++ как перегрузка операторов и добавим в наш класс следующие функции:

class CValue
{
.....
public:
 CValue& operator=(int val) { return Val(val); }
 CValue& operator=(const char* string) { return Str(string); }
 operator int() const { return nVal; }
 operator char*() const { return str; }
};

Это дало нам еще один способ использования объектов этого типа:

CValue value;
// инициализация
value=20; // происходит вызов перегруженной
// функции CValue::operator=(int val)
value="string"; // происходит вызов перегруженной
// функции CValue::operator=(char* string)

// вот так теперь можно получить значения
// соответствующих переменных класса
int n=value; // неявное преобразование value к типу int
// n будет равно value.nVal
char *str=value; // неявное преобразование value к типу char*
// str будет равно value.str

Неправда ли это становится очень похоже на Бейсик? В конце приведу еще один пример типа, который можно использовать способом, аналогичным при использовании переменных в VB. Как известно, VB "равнодушен" к типу переменных - переменной можно присвоить и 5 и "25". В одном случае произойдет неявное преобразование из строки в число, в другом наоборот. То есть, я хочу сказать, что VB является языком со слабым контролем типов в отличие от C++, обладающим строгим контролем типов. Если кто-то скажет, что это - недостаток, то я его адресую к [1]. Примером, как можно "обойти" этот "недостаток", может служить этот шаблонный класс:

template<class T> class CVBValue
{
 T m_val;
public:
 CVBValue() {};
 CVBValue(T val) { m_val=val; }
 T Val() const { return m_val; }
 CVBValue& operator=(T val) { m_val=val; return *this; }
 CVBValue& operator=(char* str)
 {
  // тут происходит преобразование из char* в тип T
  // если, конечно, известно как это сделать
  return *this;
 }
 operator T() const { return m_val; }
};

Использование этого класса происходит уже известным вам способом:

CVBValue<double> val; // создание экземпляра класса
val=2.5;
val="1.2345"; // преобразование из строки в тип double
double d=val; // получение текущего значения

Дальнейшее расширение класса зависит только от вашего воображения. Хочется вас предостеречь от излишнего упрощения использования типов. Программисты на VB могут ужаснуться, когда узнают, сколько может быть скрыто строчек кода за невинным, на первый взгляд, присваиванием. Но вы теперь это прекрасно осознаете и понимаете, что, чем более сложный код, вы напишете, тем больше вероятность появления ошибок. В данном случае, я имею в виду ошибки, появление которых можно обнаружить только во время исполнения программы. Что произойдет в описанном выше примере, если написать val="string"? В лучшем случае ничего, но вообще-то может возникнуть исключение (возможно в случае нехватки памяти). Это вынуждает нас помещать обычное приравнивание в блок

try {
 val="1.95";
}
catch (...) { }

Но так ли часто вы это делаете в своих программах? Наглядность программы тоже страдает: переменной, которая, как кажется, хранит число, вы приравниваете строку. Как я уже говорил, для Бейсика это может быть естественно, а для C++ -противоестественно. Отсюда вывод: помещайте потенциально опасный код в функции, а перегрузку операторов реализуйте как можно проще.

Литература

1. Страуструп Б. Язык программирования C++. М.: "Невский Диалект" - "Издательство БИНОМ", 1999.
2. Страуструп Б. Дизайн и эволюция языка C++. М.: ДМК Пресс, 2000.

____________________

Что ж, достаточно интересная статья. Большое спасибо автору. Думаю, теперь многим читателям хотелось бы сравнить этот способ с описанным в предыдущем выпуске. На мой взгляд, вышеописанный способ проще и нагляднее; кроме того, он лишен некоторых недостатков предыдущего метода (который, действительно, больше ориентирован на COM).
Но есть недостатки, к сожалению, присущие обоим методам: это некоторое снижение быстродействия, плюс увеличение расходования памяти (в основном за счет использования шаблонов). Хотя ради справедливости надо заметить, что использовать шаблоны приходится только когда вам нужны не зависящие от типа данных конструкции.
Потерю производительности можно уменьшить, если блоки проверки корректности параметров для присваивающих функций заключить между директивами условной компиляции #ifdef _DEBUG / #endif. Тогда они будут работать только в отладочной версии программы, позволяя выявить допущенные где-то в другом месте ошибки, а в окончательную сборку проекта не войдут.

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

Как задать минимальный и максимальный размер окна?

Автор: Александр Шаргин

Когда пользователь изменяет размеры окна, Windows сама запрашивает у программы минимальный и максимальный размеры, посылая окну сообщение WM_GETMINMAXINFO. При этом впараметре lParam размещается указатель на структуру MINMAXINFO, в которую и следует записать нужные значения. Затем нужно вернуть 0. Рассмотрим пример обработки сообщения WM_GETMINMAXINFO, при котором размер окна не может быть сделан меньше (100x100) и больше (300x300).

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
        ...
        case WM_GETMINMAXINFO:
        {
            MINMAXINFO *pInfo = (MINMAXINFO *)lParam;
            POINT ptMin = { 100, 100 }, ptMax = { 300, 300 };
            pInfo->ptMinTrackSize = ptMin;
            pInfo->ptMaxTrackSize = ptMax;
            return 0;
        }
        default:
            return DefWindowProc(hWnd, message, wParam, lParam);
   }
}

В MFC обработчик выглядит аналогичным образом, например:

void CMainFrame::OnGetMinMaxInfo(MINMAXINFO FAR* lpMMI)
{
    lpMMI->ptMinTrackSize = CPoint(100, 100);
    lpMMI->ptMaxTrackSize = CPoint(300, 300);

    CFrameWnd::OnGetMinMaxInfo(lpMMI);
}
ПРИМЕЧАНИЕ
Для добавления этого обработчика можно использовать ClassWizard. Если оно не появляется в списке Messages, перейдите на вкладку Class Info и установите Message filter: Window.

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Это все на сегодня. Пока!

Алекс Jenter jenter@rsdn.ru
Красноярск, 2001. Рассылка является частью проекта RSDN.


No.45

Выпуск No. 45 от 20 мая 2001 г.

Всем привет!

/ / / / СТАТЬЯ / / / / / / / / / / / / / / / / / / / / / /

Прозрачность - это просто

Автор: Виталий Брусенцев
Демонстрационный проект (57 Kb, Visual C++ 6.0)
Демонстрационная программа (45 Kb, Windows 98 и выше, режим экрана HiColor и выше)
Терминология

Прежде чем начать, убедимся, что понимаем друг друга:

  • Растровое изображение, растр (bitmap) - прямоугольная картинка, состоящая из пикселов.
  • Пиксел - минимальный элемент изображения, точка на экране или в памяти растра.
  • Прозрачность - свойство некоторых пикселов не отображаться на устройстве вывода, оставляя оригинальное изображение неизменным.
  • Полупрозрачность - такое взаимодействие пикселов, при котором видны как пикселы выводимого растра, так и фоновое изображение.
  • Спрайт - растровое изображение с прозрачными и полупрозрачными участками.

Введение

Зачем нужны растровые изображения с прозрачностью или полупрозрачностью отдельных участков? Это важные элементы графического интерфейса Windows, которые вы можете наблюдать каждый раз, когда включаете компьютер. Иконки на рабочем столе имеют прозрачные участки, что позволяет видеть "сквозь" них. Когда вы, работая в папке Windows 98, перетаскиваете какой-нибудь объект, его значок становится полупрозрачным, позволяя видеть, что в данный момент находится под ним. И, наконец, нельзя забывать о таких "графикоемких" программах, как игры. Трудно себе представить, чтобы в тщательно спроектированной плоской космической "стрелялке" все корабли имели прямоугольную форму. А при отрисовке взрывов желательно делать их полупрозрачными, приближая картинку к реальности.

Вообще - то на эту тему писали довольно много. Для понимания основных механизмов получения эффектов прозрачности рекомендую прочитать статью Рона Джери "Bitmaps with transparency" (ее можно найти в MSDN в разделе Technical Articles->Multimedia->GDI). Также рекомендую изучить находящиеся там статьи Дейла Роджерсона ("Sprites Make the World Go Round") и Германа Родента ("Animation in Win32").

К сожалению, все эти статьи разделяют общий недостаток - почтенный возраст. Цель данной статьи - показать, что с появлением Windows 95, а затем Windows 98 и Windows 2000 жить программистам стало намного проще (и интереснее!). Все приводимые примеры написаны, для удобства, с использованием библиотеки MFC, но принципы остаются общими.

Windows 95 и списки изображений

С выходом Windows 95 в распоряжении программистов оказалась удобная библиотека Common Controls. В ее составе были не только новые (теперь уже известные всем) элементы управления, но и невизуальный компонент - список изображения (Image List control). Его основное предназначение - содержать набор картинок одинакового размера. Это удобно для применения в разнообразных элементах - например, в панелях инструментов (toolbar).

Нас же больше интересует другая интересная возможность - хранить в списках изображений информацию о прозрачности. Это достигается одним из двух способов:

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

Нужно понимать, что в обоих случаях список изображений будет содержать маску прозрачности, просто при первом способе она будет создана за вас. Какой способ избрать - дело вкусов каждого программиста. Я обычно нахожу в палитре какой-нибудь ненужный цвет и назначаю его прозрачным. В большинстве случаев не везет ярко-сиреневому цвету (RGB 255,0,255).

Создание списков прозрачных изображений

Создать список изображения и загрузить в него растр с прозрачностью можно так:

    CBitmap m_Bmp;
    m_Bmp.LoadBitmap(IDB_BITMAP1);
    CImageList imgList;
    imgList.Create(cx, cy, ILC_COLOR24|ILC_MASK, 1, 0);
    imgList.Add(&m_Bmp, RGB(255,255,255));

В приводимых примерах будет предполагаться, что растровые изображения находятся в ресурсах программы и имеют глубину цвета 24 бита (16 млн. цветов). При создании списка необходимо указать размеры загружаемого растра (cx, cy), его цветовой формат (ILC_COLOR24, 16 миллионов оттенков) и признак наличия маски (ILC_MASK). Последние два параметра Create() определяют число хранимых в списке изображений и величину приращения списка при нехватке места. Макрос RGB удобен для указания цвета в 24-битовом диапазоне, в данном случае - цвета прозрачности.

В принципе, загрузить картинку можно и одним вызовом, но этот метод не поддерживает полноцветные изображения (как в нашем примере):

    CImageList imgList;
    imgList.Create(IDB_BITMAP1, cx, 0, RGB(255,255,255));

По умолчанию, такой растр ограничен 16 цветами.

Рисование с помощью Image List

Для чего списки изображений действительно полезны, так это для облегчения работы по рисованию. Вы вспомните навскидку число и порядок параметров у BitBlt()? А ее неудобство из-за необходимости подготовить дополнительный контекст устройства в памяти? Все это способно смутить не только новичка, но и более опытного программиста.

Ситуация еще более усложняется, когда мы имеем дело с прозрачностью. Необходимо выводить картинку несколько раз с различными параметрами растровых операций (Raster operation, ROP). Каждый раз, когда мы собираемся вывести на экран небольшое изображение космического корабля, необходимо проделать уйму работы.

К счастью, программисты Microsoft уже сделали ее за нас. Можно нарисовать растр, содержащийся в списке изображений, просто вызвав функцию ImageList_Draw(). С использованием MFC этот вызов выглядит, например, так:

    imgList.Draw(pDC, 0, m_drawPoint, ILD_TRANSPARENT);

Здесь pDC - указатель на контекст устройства (CDC), 0 - порядковый номер выводимого из списка изображения, m_drawPoint - координаты начала области вывода. Флаг ILD_TRANSPARENT указывает, что вывод нужно осуществлять с учетом маски прозрачности.

Для самых любознательных сообщу, что реализация эффекта прозрачности при этом достигается методом, который Рон Джери называет Black Source Method, т.е., "метод черного источника". Он позволяет выводить изображение с прозрачными участками за два вызова BitBlt() вместо трех, но требует предварительно заменить пикселы, являющиеся прозрачными, черным цветом. Поэтому, загружая растр в список изображений, вы меняете его.

В результате вызова получится примерно вот что.

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

Windows 98 и библиотека msimg32.dll

Windows 98 принесла новый простой способ вывода прозрачных изображений. Входящая в ее состав библиотека msimg32.dll содержит новые функции для получения соблазнительных графических эффектов. Для ее использования нужно подключить к проекту при сборке файл msimg32.lib.

Теперь растр с прозрачностью можно вывести за один прием с помощью функции TransparentBlt, указав прозрачный цвет в последнем параметре функции:

    CDC memDC;
    memDC.CreateCompatibleDC(pDC);
    CBitmap *temp = memDC.SelectObject(m_Bmp);
    TransparentBlt(pDC->m_hDC, x, y, dstX, dstY, memDC.m_hDC,
                   x1, y1, srcX, srcY, RGB(255,255,255));
    memDC.SelectObject(temp);

"Ничего себе - за один прием!" скажете вы и ...будете правы. Вновь появляется необходимость в совместимом контексте устройства, в котором нужно выбрать выводимый растр. Вновь - длинный (и не интуитивный) список параметров. Но зато появились возможности по управлению выводом. В данном примере x, y, x1, y1 - координаты начальной точки растра в приемнике и источнике соответственно. Параметры dstX и dstY - размеры области вывода, а srcX и srcY - ширина и высота прямоугольника, отображаемого из растра. Что если задать для них разные значения? Результат приведен здесь.

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

Добавим, что функция TransparentBlt() при выводе опирается на возможности DirectX данного устройства, что может дать прирост производительности по сравнению с традиционными методами.

СОВЕТ
Не забудьте после использования контекста устройства вновь выбрать в нем начальный растр (temp в нашем примере). В противном случае произойдет утечка графических ресурсов системы.
AlphaBlend(): "Полу" - не обязательно 1/2

Все вышеописанные методы работают только с одной моделью прозрачности, называемой в компьютерной графике Chroma Key. Это означает, что прозрачным назначается определенный цвет. Другая, более развитая, модель называется Alpha Blending. При ее использовании для описания характеристик пикселов кроме цветовых компонент (R, G, B) применяется прозрачность (Alpha). Степень прозрачности определяется обратной величиной этого параметра.

Для поддержки этого режима Windows 98, а затем и Windows 2000 и Windows ME предоставляют функцию AlphaBlend():

BOOL AlphaBlend(
  HDC hdcDest,                 // handle to destination DC
  int nXOriginDest,            // x-coord of upper-left corner
  int nYOriginDest,            // y-coord of upper-left corner
  int nWidthDest,              // destination width
  int nHeightDest,             // destination height
  HDC hdcSrc,                  // handle to source DC
  int nXOriginSrc,             // x-coord of upper-left corner
  int nYOriginSrc,             // y-coord of upper-left corner
  int nWidthSrc,               // source width
  int nHeightSrc,              // source height
  BLENDFUNCTION blendFunction  // alpha-blending function
);

Назначение ее параметров, в-общем, ясно из прототипа. Особый интерес представляет последний параметр - BLENDFUNCTION. Он представляет собой структуру (всего из четырех байт), определяющую режим вывода.

typedefstruct _BLENDFUNCTION {
  BYTE     BlendOp;
  BYTE     BlendFlags;
  BYTE     SourceConstantAlpha;
  BYTE     AlphaFormat;
}BLENDFUNCTION, *PBLENDFUNCTION, *LPBLENDFUNCTION;

Что это такое? Дело в том, что AlphaBlend() может работать в двух разных режимах.

Простой режим (общая прозрачность)

Первый (и наиболее простой в использовании) режим работы AlphaBlend() предполагает, что значение Alpha задано для всей картинки. В таком случае, оно применяется ко всем пикселам без исключения.

Формат BLENDFUNCTION в этом случае:

     BLENDFUNCTION blend;
     blend.BlendOp = AC_SRC_OVER;
     blend.BlendFlags = 0;
     blend.AlphaFormat = 0;
     blend.SourceConstantAlpha = 180;

Для поля BlendOp в данный момент определено только одно допустимое значение - AC_SRC_OVER.
Поле BlendFlags должно содержать 0.
Плохо документированный параметр AlphaFormat определяет взаимодействие пикселов источника и приемника, о чем мы еще поговорим далее.
Параметр SourceConstantAlpha определяет степень непрозрачности. Задав для этого поля 0, вы не увидите свой растр вообще. Максимальное значение, умещающееся в тип BYTE, равно 255. При этом выводимый растр полностью перекроет область назначения. Но зачем вам, в таком случае, AlphaBlend()? И это значение используется, в-основном, для второго режима.

Режим с альфа-каналом

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

Сложности возникнут вот с чем. Мне неизвестен ни один графический пакет, позволяющий сохранять растры с альфа-каналом в формате, пригодном для AlphaBlend(). При создании такого растра программно мы можем сохранить его на диск (в формате Windows Bitmap 32-bit). Он прочитывается популярными программами типа ACDSee, но функции LoadBitmap() и LoadImage() отказываются его загружать. При попытке поместить его в ресурс rc.exe у меня вывалился с сообщением Internal error...

Но унывать не стоит. Мы можем подготовить растр в памяти, скомбинировав, например, два растра - с изображением объекта и его тени. К счастью, функция AlphaBlend() может работать с растрами, созданными с помощью CreateDIBSection():

HBITMAP CreateDIBSection(
  HDC hdc,                 // handle to DC
  CONST BITMAPINFO *pbmi,  // bitmap data
  UINT iUsage,             // data type indicator
  VOID **ppvBits,          // bit values
  HANDLE hSection,         // handle to file mapping object
  DWORD dwOffset           // offset to bitmap bit values
);

Здесь hdc - контекст, совместимый с устройством вывода, pbmi - указатель на структуру BITMAPINFO с информацией о размерах и цветовой глубине создаваемого растра. Параметр iUsage определяет, что будет содержаться в буфере растра - индексы в палитре цветов (DIB_PAL_COLORS) или прямые их значения (DIB_RGB_COLORS). Очевидно, что второе - для режима TrueColor палитра не нужна.
Указатель на созданный буфер при успешном вызове будет возвращен в параметре ppvBits.
В параметрах hSection и dwOffset при работе с растром в памяти необходимо указывать 0.

ПРИМЕЧАНИЕ
Все вышесказанное не означает, что спрайты с альфа-каналом нельзя хранить в ресурсах программы. Вы можете включить их как custom resource, загрузить с помощью LoadResource() и заполнить полученными данными буфер, созданный с помощью CreateDIBSection(). Но не забывайте, что это значительно увеличит размер вашего исполняемого модуля. Кроме того, можно рассмотреть вариант подгрузки предварительно рассчитанных в подобной программе растров из внешних файлов - средствами библиотеки исполнения или используя параметры hSection и dwOffset при вызове CreateDIBSection().

Использование CreateDIBSection() облегчает доступ к битам изображения: в противном случае такие картинки можно было бы вывести только в режиме экрана True Color 32 bit. Для обращения с таким форматом данных идеально подходит структура RGBQUAD:

typedefstruct tagRGBQUAD {
  BYTE    rgbBlue;
  BYTE    rgbGreen;
  BYTE    rgbRed;
  BYTE    rgbReserved;
} RGBQUAD;

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

Результат приведен здесь.

Детали работы с битами растров мы опустим (все это можно найти в прилагаемом проекте). Отмечу только, что для вывода использовался такой формат BLENDFUNCTION:

    blend.BlendOp = AC_SRC_OVER;
    blend.BlendFlags = 0;
    blend.AlphaFormat = AC_SRC_NO_PREMULT_ALPHA;
    blend.SourceConstantAlpha = 255;

Параметр AC_SRC_NO_PREMULT_ALPHA не описан в MSDN за январь 2000 года и найден экспериментально (и подглядыванием в wingdi.h :) При его задании используется альфа-канал растра источника и не используется альфа-канал приемника (возможно и такое).

СОВЕТ
При использовании альфа-канала у вас все равно остается возможность без пересчета битов растра изменять прозрачность всей картинки - SourceConstantAlpha работает и в этом случае.

И в завершение напомню, что AlphaBlend() также требует включения в проект при сборке библиотеки импорта msimg32.lib, которая отсутствует в Windows 95.

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

Как создать многострочный тултип?

Автор: Александр Шаргин

Начиная с версии 4.70 библиотеки Comctl32.dll тултипы поддерживают многострочный режим работы. По умолчанию он выключен, и всё, что требуется от нас - активизировать его. Для этого предназначено сообщение TTM_SETMAXTIPWIDTH, которое позволяет задать ширину тултипа (в пикселях). По умолчанию ширина установлена в -1, что соответствует однострочному режиму работы. В этом режиме тултип игнорирует все пары '\r\n' в тексте подсказки, выдавая его в одну строку. Задание любого положительного значения ширины переводит тултип в многострочный режим работы.

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

SendMessage(hTip, TTM_SETMAXTIPWIDTH, 0, (LPARAM)0xFFFFFF);

В MFC аналогичного эффекта можно добиться, используя фукцию CToolTipCtrl::SetMaxTipWidth. Единственный параметр, который она получает - новое значение ширины тултипа. Например:

// m_tt - объект класса CToolTipCtrl
m_tt.SetMaxTipWidth(0xFFFFFF);

Проблемы возникают только в том случае, когда вы используете встроенную поддержку тултипов класса CWnd. В этом случае тултип создаётся и уничтожается в недрах MFC, причём документированного способа добраться до него не существует. Выйти из положения можно, воспользовавшись недокументированным: MFC сохраняет указатель на созданный ею объект класса CToolTipCtrl в структуре _AFX_THREAD_STATE, и можно получить к нему доступ, используя выражение AfxGetThreadState()->m_pToolTip.

Вторая проблема состоит в том, что MFC сама следит за временем жизни тултипа, и мы не можем точно знать, когда он уничтожается и создаётся заново. Поэтому ширину тултипа необходимо заново задавать всякий раз, когда тултип "собирается" появиться на экране. Удобнее всего делать это в ответ на уведомление TTN_NEEDTEXT, например:

BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
    ON_NOTIFY_EX_RANGE(TTN_NEEDTEXT, 0, 0xFFFFFFFF, OnToolTipText)
END_MESSAGE_MAP()

BOOL CMainFrame::OnToolTipText(...)
{
    CToolTipCtrl* ptt = AfxGetThreadState()->m_pToolTip;

    ptt->SetMaxTipWidth(0xFFFFFF);

    return CFrameWnd::OnToolTipText(...);
}

/ / / / ОБРАТНАЯ СВЯЗЬ / / / / / / / / / / / / / / / /

Здравствуйте!

Благодарю Вас за прекрасную и очень полезную рассылку! Полагаю, что мою благодарность разделяют очень многие ее читатели.

В номере 36 от 11.03.2001 г. рассылки "Программирование на VC++" Сергей Петухов задал вопрос о русификации элементов окна предварительного просмотра перед печатью. В номере 38 от 25.03.2001 г. на него дал обстоятельный ответ Александр Шаргин. Предложенный в ответе метод решения проблемы имеет огромное неоспоримое достоинство - он работоспособен! Однако, не лишен и некоторых очевидных недостатков, связанных с необходимостью ручного редактирования информации ресурсных файлов в каждом из проектов, ориентированных на русскоязычного пользователя. Такой метод пригоден скорее для случаев, когда нужно изменить именно вид стандартного интерфейса MFC в соответствии с требованиями конкретного приложения, а не его язык.

MSDN предлагает иной путь решения. Кратко он освещен в статье "Localization of MFC Components" (MSDN/Visual C++ Documentation/Reference/MFC Library and Templates/MFC Library/MFC Technical notes/TN057). Чуть подробнее последовательность необходимых действий описан в статье "HOWTO: #include the Localized MFC Resources in an EXE or DLL" (ID: Q198536) Knowledge Base.

Суть заключается в том, что все языкозависимые ресурсы MFC размещены в каталогах MFC\INCLUDE\L.* и при статическом присоединении ресурсов MFC к проекту, должны использоваться именно они. Для подключения нужного подкаталога достаточно указать его в командной строке компилятора ресурсов с ключом /I (пример из MSDN /IC:\PROGRAM FILES\DEVSTUDIO\VC\MFC\INCLUDE\L.DEU).

Все довольно просто. Создаем новый подкаталог MFC\INCLUDE\L.RUS, записываем в него по образу других языков семь файлов *.rc и русифицируем их в текстовом редакторе (в редакторе ресурсов VS это сделать не удастся, т.к. файлы защищены от изменения специальной директивой). Затем указываем на вкладке свойств проекта Resources (поле Additional resource include directories) наш новый русскоязычный каталог (например, C:\PROGRAM FILES\DEVSTUDIO\VC\MFC\INCLUDE\L.RUS). В статье из KB рекомендуется еще и удалить все директивы _AFXDLL препроцессора, которые видны на той же вкладке, но в моих приложениях, со статическим присоединением ресурсов MFC, таковые не попадались. Затем открываем меню "View" и выбираем пункт "Resource Includes" и в окне "Compile-time directives" изменяем код языка и номер кодовой страницы. Данные о кодах можно найти в файле "winnt.h". Там они представлены в шестнадцатеричном формате и их следует привести к основанию 10. Для русского языка нужные строки будут выглядеть так:

LANUGAGE 25, 1
#pragma code_page(1251)

После этого проект можно собирать.

В статье из KB сказано, что в том же меню "View" - "Resource Includes" можно указать измененные маршруты заголовочных файлов, например, #include "l.rus/afxprint.rc" вместо #include "afxprint.rc". Вероятно, если это сделать, отпадет необходимость в указании маршрута каталога с русифицированными ресурсами на вкладке "Resources" свойств проекта. Кроме того, в той же статье рекомендуется с помощью редактора ресурсов удалить из раздела "String Table" все неспецифичные для конкретного приложения строки, сгенерериванные AppWizard. Полагаю, это можно не делать. В крайнем случае, на необходимость такой операции укажет компилятор.

Таким образом, нужно один раз перевести на русский язык содержимое нескольких файлов ресурсов, а затем лишь указывать в проектах нужный язык и маршрут каталога с русифицированными файлами.
- Андрей Шуклин

Большое спасибо, Андрей.

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Это все на сегодня. Пока!

Алекс Jenter jenter@rsdn.ru
Красноярск, 2001. Рассылка является частью проекта RSDN.


No.46

Выпуск No. 46 от 27 мая 2001 г.

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

Мой коллега Александр Шаргин решил присоединиться к дисскуссии по поводу свойств в С++:

/ / / / ОБРАТНАЯ СВЯЗЬ / / / / / / / / / / / / / / / / / / / / / /

Свойства "в стиле VB" невозможно заменить перегрузкой оператора присваивания и приведения типа. Если в классе содержится 10 свойств типа int, мы неизбежно заходим в тупик. Что касается полезности этой концепции, она признана многими разработчиками компонентно-ориентированных средств разработки (кроме VB есть Delphi и BCB, в которые свойства вводились отнюдь не только для поддержки COM, свойства есть в новом языке C# от Микрософт и т. д.). Конечно, свойства - не революция. Вместо них можно использовать пару методов Set/Get. Но они делают смысл происходящего в программе понятнее:

wnd.style |= WS_VISIBLE;

вместо

wnd.SetStyle(wnd.GetStyle() | WS_VISIBLE);

Кроме того, они развивают концепцию сокрытия деталей реализации от пользователя класса: для него свойство выглядит как обычная переменная-член, и он даже не догадывается, что для её реализации используются функции. Не секрет, что многие программисты не любят методы Set/Get и продолжают использовать открытые члены, несмотря на все призывы Страуструпа и иже с ним. Даже в книгах по программированию этот "неправильный" подход встречается сплошь и рядом. Свойства позволяют найти компромисс между этим чисто человеческим нежеланием и важными принципами ООП. В выигрыше оказываются все.

Что касается реализации свойств с С++, здесь перед нами встают проблемы. Если вопрос портирования прграммы для нас не актуален, и мы ограничиваемся VC, я не вижу смысла игнорировать его расширенные возможности. Использование __deсlspec(property) в этом случае является исключительно делом вкуса программиста. Принципиальных возражений против его использования нет.

А вот "самодельные" свойства приносят в жертву эффективность программы, и поэтому мы должны или отказаться от них, или улучшить, убрав лишние затраты. Вот пример возможного улучшения.

#include <stddef.h>

//***************************
// Макрос для свойства
#define PROPERTY(ownertype, proptype, prop, getfunc, setfunc)
\
class __property##prop
\
{
\
public:
\
    operator proptype()
\

{
\
        return ((ownertype *)((char *)this - offsetof(ownertype,
prop)))->getfunc(); \
    }
\
    void operator=(proptype data)
\

{
\
        ((ownertype *)((char *)this - offsetof(ownertype,
prop)))->setfunc(data);    \
    }
\
};
\
friend __property##prop;
\
__property##prop prop

//***************************
// Пример использования
class CValue
{
private:
    char m_value;

public:
    int get_Value()
    {
        return m_value;    // Или более сложная логика
    }
    void put_Value(int value)
    {
        m_value = value;     // Или более сложная логика
    }

    // Свойство Value типа int, использующее функции
    // get_Value и put_Value класса CValue
    PROPERTY(CValue, int, Value, get_Value, put_Value);
};

int main(int argc, char* argv[])
{
CValue val;

/*
Здесь вызывается оператор присваивания переменной-члена val.Value,
и, следовательно, функция val.put_Value()
*/
val.Value = 50;

/*
Здесь вызывается оператор приведения типа переменной-члена val.Value,
и, следовательно, функция val.get_Value()
*/
int z = val.Value;

return 0;
}

В этом случае ликвидируются как временные затраты (все функции класса __property##prop являются встроенными), так и затраты по памяти (к сожалению, только теоретически, так как VC "не умеет" создавать объекты класса нулевой длины). Кроме того, следует заметить, что полученное свойство не является полным эквивалентом __declspec. Например, нельзя написать

val.Value += 50;

Этот недостаток присущ и реализации в выпуске 43. Чтобы устранить его, придётся перегружать множество операторов, а потом, возможно, отлавливать множество тонких ошибок. Вот почему я считаю, что "самодельные" свойства представляют скорее теоретический интерес. Это, однако, не повод провозглашать ненужной саму концепцию свойств, аргументируя это мощью и могуществом языка C++, который в них не нуждается.
- Александр Шаргин

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

Как программно создать источник данных?

Автор: Игорь Вартанов

Для этой цели служит функция SQLConfigDataSource(). Она позволяет создать пользовательский или системный источник данных (DSN - DataSource Name). Эта же функция позволяет модифицировать или удалить DSN.

BOOL SQLConfigDataSource(
     HWND      hwndParent,
     WORD      fRequest,
     LPCSTR    lpszDriver,
     LPCSTR    lpszAttributes);

Здесь hwndParent - хэндл окна, которому будут направляться сообщения об ошибках (в случае использования NULL эти сообщения будут подавляться), fRequest - тип выполняемого действия (к примеру, ODBC_ADD_DSN - добавить пользовательский DSN, ODBC_ADD_SYS_DSN - добавить системный DSN), lpszDriver - точное имя драйвера ODBC, так, как оно выглядит в диалоге настройки ODBC DSN, например "Microsoft Access Driver (*.mdb)" или "SQL Server". Строка lpszAttributes содержит основные параметры подключения к источнику данных:

  • DSN - название создаваемого источника данных
  • UID - имя пользователя
  • DATABASE - имя базы данных
  • PWD - пароль для подключения

Параметры разделены между собой символом '\0', конец строки отмечается дополнительным символом '\0'.

DSN=CustomDsn\0UID=username\0PWD=password\0DATABASE=CustomDataBase\0\0

Обязательным является имя DSN. Все остальные параметры могут быть запрошены при подключении к источнику данных. Хотя различные драйверы ODBC в этом отношении могут вести себя по-разному - например, для драйвера MS SQLServer обязательным параметром также является и имя сервера.

///////////////////////////////////////////////////////////
////  Пример создания источника данных для
//  ODBC драйвера для Microsoft Excel 97
//
#include <windows.h>
#include <odbcinst.h>
#pragma comment(lib, "odbccp32")
#pragma comment(lib, "user32")

void main()
{
    char* driver = "Microsoft Excel Driver (*.xls)";
    char* params = "DSN=MyTable\0DefaultDir=D:\\Document\0""DBQ=D:\\Document\\MyTable.xls\0";

    // Создадим пользовательский DSN
    SQLConfigDataSource(NULL, ODBC_ADD_DSN, driver, params);
}

Надеюсь, для Вас не будет слишком большим сюрпризом узнать, что всю информацию об источниках данных и драйверах ODBC Windows хранит в реестре. А если быть совсем точным, то в ключах

HKEY_CURRENT_USER\Software\ODBC\ODBC.INI      (пользовательские DSN)
HKEY_LOCAL_MACHINE\SOFTWARE\ODBC\ODBC.INI     (системные DSN)
HKEY_LOCAL_MACHINE\SOFTWARE\ODBC\ODBCINST.INI (драйверы ODBS)

Информацию о том, как правильно заполнять строку params, можно извлечь самостоятельно, создавая при помощи ODBC-администратора источники данных для различных драйверов Вашей системы и анализируя состав параметров и присвоенные им значения ключей реестра, относящихся к созданным источникам. Надеюсь также, что не сильно шокирую Вас, если скажу, что приведенный выше пример был написан именно так.

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Всего доброго и до встречи через неделю!

Алекс Jenter jenter@rsdn.ru
Красноярск, 2001. Рассылка является частью проекта RSDN.


No.47

Выпуск No. 47 от 3 июня 2001 г.

Всем привет!

/ / / / СТАТЬЯ / / / / / / / / / / / / / / / / / / / / / /

Хуки в Win32

Автор: Kyle Marsh
Перевод: Олег Быков
Введение

В операционной системе MicrosoftR WindowsT хуком называется механизм перехвата особой функцией событий (таких как сообщения, ввод с мыши или клавиатуры) до того, как они дойдут до приложения. Эта функция может затем реагировать на события и, в некоторых случаях, изменять или отменять их. Функции, получающие уведомления о событиях, называются фильтрующими функциями и различаются по типам перехватываемых ими событий. Пример - фильтрующая функция для перехвата всех событий мыши или клавиатуры. Чтобы Windows смогла вызывать функцию-фильтр, эта функция должна быть установлена - то есть, прикреплена - к хуку (например, к клавиатурному хуку). Прикрепление одной или нескольких фильтрующих функций к какому-нибудь хуку называется установкой хука. Если к одному хуку прикреплено несколько фильтрующих функций, Windows реализует очередь функций, причем функция, прикрепленная последней, оказывается в начале очереди, а самая первая функция - в ее конце.

Когда к хуку прикреплена одна или более функций-фильтров и происходит событие, приводящее к срабатыванию хука, Windows вызывает первую функцию из очереди функций-фильтров. Это действие называется вызовом хука. К примеру, если к хуку CBT прикреплена функция и происходит событие, после которого срабатывает хук (допустим, идет создание окна), Windows вызывает CBT-хук, то есть первую функцию из его очереди.

Для установки и доступа к фильтрующим функциям приложения используют функции SetWindowsHookEx и UnhookWindowsHookEx.

Хуки предоставляют мощные возможности для приложений Windows. Приложения могут использовать хуки в следующих целях:

  • Обрабатывать или изменять все сообщения, предназначенные для всех диалоговых окон (dialog box), информационных окон (message box), полос прокрутки (scroll bar), или меню одного приложения (WH_MSGFILTER).
  • Обрабатывать или изменять все сообщения, предназначенные для всех диалоговых окон, информационных окон, полос прокрутки, или меню всей системы (WH_SYSMSGFILTER).
  • Обрабатывать или изменять все сообщения в системе (все виды сообщений), получаемые функциями GetMessage или PeekMessage (WH_GETMESSAGE).
  • Обрабатывать или изменять все сообщения (любого типа), посылаемые вызовом функции SendMessage (WH_CALLWNDPROC).
  • Записывать или проигрывать клавиатурные и мышиные события (WH_JOURNALRECORD, WH_JOURNALPLAYBACK).
  • Обрабатывать, изменять или удалять клавиатурные события (WH_KEYBOARD).
  • Обрабатывать, изменять или отменять события мыши (WH_MOUSE).
  • Реагировать на определенные действия системы, делая возможным разработку приложений компьютерного обучения - computer-based training (WH_CBT).
  • Предотвратить вызов другой функции-фильтра (WH_DEBUG).

Приложения уже используют хуки для следующих целей:

  • Добавить поддержку кнопки F1 для меню, диалоговых и информационных окон (WH_MSGFILTER).
  • Обеспечить запись и воспроизведение событий мыши и клавиатуры, часто называемых макросами. Например, программа Windows Recorder использует хуки для записи и воспроизведения (WH_JOURNALRECORD, WH_JOURNALPLAYBACK).
  • Следить за сообщениями, чтобы определить, какие сообщения предназначены определенному окну или какие действия генерирует сообщение (WH_GETMESSAGE, WH_CALLWNDPROC). Утилита Spy из Win32T Software Development Kit (SDK) for Windows NTT использует для этих целей хуки. Исходные тексты Spy можно найти в SDK.
  • Симулировать мышиный и клавиатурный ввод (WH_JOURNALPLAYBACK). Хуки - единственный надежный способ симуляции этих действий. Если попытаться имитировать их через посылку сообщений, не будет происходить обновление состояния клавиатуры или мыши во внутренних структурах Windows, что может привести к непредсказуемому поведению. Если для воспроизведения клавиатурных или мышиных событий используются хуки, эти события обрабатываются в точности так, как и настоящий ввод с клавиатуры или мыши. Microsoft Excel использует хуки для реализации макрофункции SEND.KEYS.
  • Сделать возможным использование CBT приложениями Windows (WH_CBT). Хук WH_CBT значительно облегчает разработку CBT-приложений.
Как пользоваться хуками

Чтобы пользоваться хуками, вам необходимо знать следующее:

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

Приложения Windows используют функции SetWindowsHookEx, UnhookWindowsHookEx, и CallNextHookEx для управления очередью функций-фильтров хука. До версии 3.1 Windows предоставляла для управления хуками функции SetWindowsHook, UnhookWindowsHook, и DefHookProc. Хотя эти функции до сих пор реализованы в Win32, у них меньше возможностей, чем у их новых (Ex) версий. Всегда старайтесь использовать только эти новые функции в своих проектах.

SetWindowsHookEx и UnhookWindowsHookEx описаны ниже. Обратитесь к разделу "Вызов следующей функции в очереди фильтрующих функций" за информацией по CallNextHookEx.

SetWindowsHookEx

Функция SetWindowsHookEx добавляет функцию-фильтр к хуку. Эта функция принимает четыре аргумента:

  • Целочисленный код, описывающий хук, к которому будет прикреплена фильтрующая функция. Эти коды определены в WINUSER.H и будут описаны позднее.
  • Адрес функции-фильтра. Эта функция должна быть описана как экспортируемая включением ее в секцию EXPORTS файла определения приложения или библиотеки динамической линковки (DLL), или использованием соответствующих опций компилятора.
  • Хэндл модуля, содержащего фильтрующую функцию. В Win32 (в отличие от Win16), этот параметр должен быть NULL при установке хука на поток (см. ниже), но данное требование не является строго обязательным, как указано в документации. При установке хука для всей системы или для потока в другом процессе, нужно использовать хэндл DLL, содержащей функцию-фильтр.
  • Идентификатор потока, для которого устанавливается хук. Если этот идентификатор ненулевой, установленная фильтрующая функция будет вызываться только в контексте указанного потока. Если идентификатор равен нулю, установленная функция имеет системную область видимости и может быть вызвана в контексте любого потока в системе. Приложение или библиотека могут использовать функцию GetCurrentThreadId для получения идентификатора текущего потока.

Некоторые хуки могут быть установлены только с системной областью видимости, некоторые можно устанавливать как для всей системы, так и для одного потока, как показано в следующей таблице.

Хук | Область видимости
WH_CALLWNDPROC | Поток или вся система
WH_CBT | Поток или вся система
WH_DEBUG | Поток или вся система
WH_GETMESSAGE | Поток или вся система
WH_JOURNALRECORD | Только система
WH_JOURNALPLAYBACK | Только система
WH_FOREGROUNDIDLE | Поток или вся система
WH_SHELL | Поток или вся система
WH_KEYBOARD | Поток или вся система
WH_MOUSE | Поток или вся система
WH_MSGFILTER | Поток или вся система
WH_SYSMSGFILTER | Только система

Для любого данного типа хука, первыми вызываются хуки потоков, и только затем системные хуки.

Есть несколько причин, по которым лучше использовать потоковые хуки вместо системных. Хуки потоков:

  • Не создают лишней работы приложениям, которые не заинтересованы в вызове хука.
  • Не помещают все события, относящиеся к хуку, в очередь (так, чтобы они поступали не одновременно, а одно за другим). Например, если приложение установит клавиатурный хук для всей системы, то все клавиатурные сообщения будут пропущены через фильтрующую функцию этого хука, оставляя неиспользованными системные возможности многопотоковой обработки ввода. Если эта функция прекратит обрабатывать клавиатурные события, система будет выглядеть зависшей, хотя на самом деле и не зависнет. Пользователь всегда сможет использовать комбинацию CTRL+ALT+DEL для того, чтобы выйти из системы (log-out) и решить проблему, но ему это вряд ли понравится. К тому же, пользователь может не знать, что подобную ситуацию можно решить, войдя в систему под другим именем (log-out/log-in).
  • Не требуют нахождения функции-фильтра в отдельной DLL. Все системные хуки и хуки для потоков в другом приложении должны находиться в DLL.
  • Им не нужно разделять данные между DLL, загруженными в разные процессы. Фильтрующие функции с системной областью видимости, которые обязаны находиться в DLL, должны к тому же разделять необходимые данные с другими процессами. Так как такое поведение не является типичным для DLL, вы должны принимать специальные меры предосторожности при реализации системных фильтрующих функций. Если функция-фильтр не умеет разделять данные и неправильно использует данные в другом процессе, этот процесс может рухнуть.

SetWindowsHookEx возвращает хэндл установленного хука (тип HHOOK). Приложение или библиотека должны использовать этот хэндл для вызова функции UnhookWindowsHookEx. SetWindowsHookEx возвращает NULL если она не смогла добавить функцию к хуку. SetWindowsHookEx также устанавливает код последней ошибки в одно из следующих значений для индикации неудачного завершения функции.

  • ERROR_INVALID_HOOK_FILTER: Неверный код хука.
  • ERROR_INVALID_FILTER_PROC: Неверная фильтрующая функция.
  • ERROR_HOOK_NEEDS_HMOD: Глобальный хук устанавливается с параметром hInstance, равным NULL либо локальный хук устанавливается для потока, который не принадлежит данному приложению.
  • ERROR_GLOBAL_ONLY_HOOK: Хук, который может быть только системным, устанавливается как потоковый.
  • ERROR_INVALID_PARAMETER: Неверный идентификатор потока.
  • ERROR_JOURNAL_HOOK_SET: Для регистрационного хука (journal hook) уже установлена фильтрующая функция. В любой момент времени может быть установлен только один записывающий или воспроизводящий хук. Этот код ошибки может также означать, что приложение пытается установить регистрационный хук в то время, как запущен хранитель экрана.
  • ERROR_MOD_NOT_FOUND: Параметр hInstance в случае, когда хук является глобальным, не ссылался на библиотеку. (На самом деле, это значение означает лишь, что модуль User не смог обнаружить данный хэндл в списке модулей.)
  • Любое другое значение: Система безопасности не позволяет установить данный хук, либо в системе закончилась память.

Windows сама заботится об организации очереди функций-фильтров (см. рисунок ниже), не доверяя функциям хранение адресов следующих функций в очереди (как поступали Windows до версии 3.1). Таким образом, система хуков в Windows 3.1 и более поздних версий стала гораздо яснее. Плюс к тому, факт хранения цепочки функций-фильтров внутри Windows значительно улучшило производительность.

Цепочка функций-фильтров в Windows

UnhookWindowsHookEx

Для удаления функции-фильтра из очереди хука вызовите функцию UnhookWindowsHookEx. Эта функция принимает хэндл хука, полученный от SetWindowsHookEx и возвращает логическое значение, показывающее успех операции. На данный момент UnhookWindowsHookEx всегда возвращает TRUE.

Фильтрующие функции

Фильтрующие (хуковые) функции - это функции, прикрепленные к хуку. Из-за того, что эти функции вызываются Windows, а не приложением, их часто называют функциями обратного вызова (callback functions). Из соображений целостности изложения, эта статья использует термин фильтрующие функции (или функции-фильтры).

Все фильтрующие функции должны быть описаны следующим образом:

LRESULT CALLBACK FilterFunc(int nCode, WPARAM wParam, LPARAM lParam)

Все функции-фильтры должны возвращать LONG. Вместо FilterFunc должно стоять имя вашей фильтрующей функции.

Параметры

Фильтрующие функции принимают три параметра: nСode (код хука), wParam, и lParam. Код хука - это целое значение, которое передает функции дополнительную информацию. К примеру, код хука может описывать событие, которое привело к срабатыванию хука.

В первых версиях Windows (до 3.1), код хука указывал, должна функция-фильтр обработать событие сама или вызвать DefHookProc. Если код хука меньше нуля, фильтр не должен был обрабатывать событие, а должен был вызвать DefHookProc, передавая ей три своих параметра без изменений. Windows использовала отрицательные коды для организации цепочки функций-фильтров с помощью приложений.

Windows 3.1 также требует при отрицательном коде хука вызывать CallNextHookEx с неизмененными параметрами, плюс к тому функция должна вернуть значение, которое вернет CallNextHookEx. Но Windows 3.1 никогда не посылает фильтрующим функциям отрицательных кодов.

Второй параметр функции-фильтра, wParam, имеет тип WPARAM, и третий параметр, lParam, имеет тип LPARAM. Эти параметры передают информацию фильтрующим функциям. У каждого хука значения wParam и lParam различаются. Например, фильтры хука WH_KEYBOARD получают в wParam виртуальный код клавиши, а в lParam - состояние клавиатуры на момент нажатия клавиши. Фильтрующие функции, прикрепленные к хуку WH_MSGFILTER получают в wParam значение NULL, а в lParam - указатель на структуру, описывающую сообщение. За полным описанием значений аргументов каждого типа хука обратитесь к Win32 SDK for Windows NT, руководствуясь списком фильтрующих функций, приведенным ниже.

Хук | Имя статьи с описанием фильтрующей функции в SDK
WH_CALLWNDPROC | CallWndProc
WH_CBT | CBTProc
WH_DEBUG | DebugProc
WH_GETMESSAGE | GetMsgProc
WH_JOURNALRECORD | JournalRecordProc
WH_JOURNALPLAYBACK | JournalPlaybackProc
WH_SHELL | ShellProc
WH_KEYBOARD | KeyboardProc
WH_MOUSE | MouseProc
WH_MSGFILTER | MessageProc
WH_SYSMSGFILTER | SysMsgProc
Вызов следующей функции в цепочке фильтрующих функций

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

Первый параметр - это значение, возвращенное функцией SetWindowsHookEx. В настоящее время Windows игнорирует это значение, но в будущем это может измениться.

Следующие три параметра - nCode, wParam, и lParam - Windows передает дальше по цепочке функций.

Windows хранит в своих внутренних структурах цепочку фильтрующих функций и следит за тем, какая функция вызывается в настоящий момент. При вызове CallNextHookEx Windows определяет следующую функцию в очереди и вызывает ее.

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

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

Фильтры в DLL

Фильтрующие функции с системной областью видимости должны быть реализованы в DLL. В Win16 было возможно (хотя и не рекомендовалось) установить системный хук, находящийся в приложении. Это не сработает в Win32. Ни в коем случае не устанавливайте глобальных фильтров, не находящихся в отдельной DLL, даже если это где-нибудь и работает. Регистрационные хуки, WH_JOURNALRECORD и WH_JOURNALPLAYBACK, являются исключением из правила. Из-за того, как Windows вызывает эти хуки, их фильтрующим функциям не обязательно находиться в DLL.

Фильтрующие функции для хуков с системной областью видимости должны быть готовы разделять свои данные между разными процессами, из которых они запускаются. Каждая DLL отображается в адресное пространство использующего ее клиентского процесса. Глобальные переменные в DLL будут таковыми лишь в пределах одного экземпляра приложения, если только они не будут находиться в разделяемом сегменте данных (shared data section). Например, библиотека HOOKSDLL.DLL в примере Hooks использует две глобальные переменные:

  • Хэндл окна для отображения сообщений.
  • Высоту строк текста в этом окне.

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

  • Использует директивы компилятора (pragma) для помещения данных в именованный сегмент данных. Заметьте, что при этом переменные должны быть обязательно инициализированы.
    // Shared DATA
  #pragma data_seg(".SHARDATA")
  static HWND hwndMain = NULL;  // Главный hwnd. Мы получим его от приложения.
  static int nLineHeight = 0;  // Высота строк в окне.
  #pragma data_seg()
  • Добавляет блок SECTIONS в .DEF-файл библиотеки:

    SECTIONS
       .SHARDATA   Read Write Shared
  • Создает .EXP-файл из .DEF-файла:

    hooksdll.exp: hooksdll.obj hooksdll.def
        $(implib) -machine:$(CPU) \
       -def:hooks.def             \
       hooksdll.obj               \
       -out:hooksdll.lib
  • Прилинковывает получившийся файл HOOKSDLL.EXP:

    hooksdll.dll: hooksdll.obj hooksdll.def hooksdll.lib hooksdll.exp
       $(link) $(linkdebug)              \
       -base:0x1C000000 \
       -dll                              \
       -entry:LibMain$(DLLENTRY)         \
       -out:hooksdll.dll                 \
       hooksdll.exp hooksdll.obj hooksdll.rbj \
       $(guilibsdll)
Типы хуков
WH_CALLWNDPROC

Windows вызывает этот хук при каждом вызове функции SendMessage. Фильтрующей функции передается код хука, показывающий, была ли произведена посылка сообщения из текущего потока, а также указатель на структуру с информацией о сообщении.

Структура CWPSTRUCT описана следующим образом:

typedef struct tagCWPSTRUCT {
    LPARAM  lParam;
    WPARAM  wParam;
    DWORD   message;
    HWND    hwnd;
} CWPSTRUCT, *PCWPSTRUCT, NEAR *NPCWPSTRUCT, FAR *LPCWPSTRUCT;

Фильтры могут обработать сообщение, но не могут изменять его (хотя это было возможно в Win16). Сообщение затем отсылается той функции, которой и предназначалось. Этот хук использует значительное количество системных ресурсов, особенно, когда он установлен с системной областью видимости, поэтому используйте его только в целях отладки.

WH_CBT

Чтобы написать приложение для интерактивного обучения (CBT application), разработчик должен координировать его работу с работой приложения, для которого оно разрабатывается. Для достижения этой цели Windows предоставляет разработчикам хук WH_CBT. Windows передает фильтрующей функции код хука, показывающий, какое произошло событие, и соответствующие этому событию данные.

Фильтр для хука WH_CBT должен знать о десяти хуковых кодах:

  • HCBT_ACTIVATE
  • HCBT_CREATEWND
  • HCBT_DESTROYWND
  • HCBT_MINMAX
  • HCBT_MOVESIZE
  • HCBT_SYSCOMMAND
  • HCBT_CLICKSKIPPED
  • HCBT_KEYSKIPPED
  • HCBT_SETFOCUS
  • HCBT_QS
HCBT_ACTIVATE

Windows вызывает хук WH_CBT с этим кодом при активации какого-нибудь окна. Когда хук WH_CBT установлен как локальный, это окно должно принадлежать потоку, на который установлен хук. Если фильтр в ответ на это событие вернет TRUE, окно не будет активизировано.

Параметр wParam содержит хэндл активизируемого окна. В lParam содержится указатель на структуру CBTACTIVATESTRUCT, которая описана следующим образом:

typedef struct tagCBTACTIVATESTRUCT
{
   BOOL    fMouse;      // TRUE, если активация наступила в результате
   //  мышиного клика; иначе FALSE.
   HWND    hWndActive;  // Содержит хэндл окна, активного
   //  в настоящий момент.
} CBTACTIVATESTRUCT, *LPCBTACTIVATESTRUCT;
HCBT_CREATEWND

Windows вызывает хук WH_CBT с этим при создании окна. Когда хук установлен как локальный, это окно должно создаваться потоком, на который установлен хук. Хук WH_CBT вызывается до того, как Windows пошлет новому окну сообщения WM_GETMINMAXINFO, WM_NCCREATE, или WM_CREATE. Таким образом, фильтрующая функция может запретить создание окна, вернув TRUE.

В параметре wParam содержится хэндл создаваемого окна. В lParam - указатель на следующую структуру.

/*
 * данные для HCBT_CREATEWND, на которые указывает lParam
 */struct CBT_CREATEWND
{
    struct tagCREATESTRUCT *lpcs; // Данные для создания
    //  нового окна.
    HWND  hwndInsertAfter;        // Хэндл окна, после которого будет
    //  добавлено это окно (Z-order).
} CBT_CREATEWND, *LPCBT_CREATEWND;

Функция-фильтр может изменить значение hwndInsertAfter или значения в lpcs.

HCBT_DESTROYWND

Windows вызывает хук WH_CBT с этим кодом перед уничтожением какого-либо окна. Если хук является локальным, это окно должно принадлежать потоку, на который установлен хук. Windows вызывает хук WH_CBT до посылки сообщения WM_DESTROY. Если функция-фильтр вернет TRUE, окно не будет уничтожено.

Параметр wParam содержит хэндл уничтожаемого окна. В lParam находится 0L.

HCBT_MINMAX

Windows вызывает хук WH_CBT с этим кодом перед минимизацией или максимизацией окна. Когда хук установлен как локальный, это окно должно принадлежать потоку, на который установлен хук. Если фильтр вернет TRUE, действие будет отменено.

В wParam передается хэндл окна, которое готовится к максимизации/минимизации. lParam содержит одну из SW_*-констант, определенных в WINUSER.H и описывающих операцию над окном.

HCBT_MOVESIZE

Windows вызывает хук WH_CBT с этим кодом перед перемещением или изменением размеров окна, сразу после того, как пользователь закончил выбор новой позиции или размеров окна. Если хук установлен как локальный, это окно должно принадлежать потоку, на который установлен хук. Если фильтр вернет TRUE, действие будет отменено.

В wParam передается хэндл перемещаемогоизменяемого окна. lParam содержит LPRECT, который указывает на новые координаты окна.

HCBT_SYSCOMMAND

Windows вызывает хук WH_CBT с этим кодом во время обработки системной команды. Если хук установлен как локальный, окно, чье системное меню вызвало данное событие, должно принадлежать потоку, на который установлен хук. Хук WH_CBT вызывается из функции DefWindowsProc. Если приложение не передает сообщение WH_SYSCOMMAND функции DefWindowsProc, это хук не получит управление. Если функция-фильтр вернет TRUE, системная команда не будет выполнена.

В wParam содержится системная команда (SC_TASKLIST, SC_HOTKEY, и так далее), готовая к выполнению. Если в wParam передается SC_HOTKEY, в младшем слове (LOWORD) lParam содержится хэндл окна, к которому относится горячая клавиша. Если в wParam передается любое другое значение и если команда системного меню была выбрана мышью, в младшем слове lParam будет находиться горизонтальная позиция, а в старшем слове (HIWORD) - вертикальная позиция указателя мыши.

Следующие системные команды приводят к срабатыванию этого хука изнутри DefWindowProc:

SC_CLOSE | Закрыть окно.
SC_HOTKEY | Активировать окно, связанное с определенной горячей клавишей.
SC_HSCROLL | Горизонтальная прокрутка.
SC_KEYMENU | Выполнить команду меню по комбинации клавиш.
SC_MAXIMIZE | Распахнуть окно.
SC_MINIMIZE | Минимизировать окно.
SC_MOUSEMENU | Выполнить команду меню по щелчку мыши.
SC_MOVE | Переместить окно.
SC_NEXTWINDOW | Перейти к следующему окну.
SC_PREVWINDOW | Перейти к предыдущему окну.
SC_RESTORE | Сохранить предыдущие координаты (контрольная точка - checkpoint).
SC_SCREENSAVE | Запустить хранитель экрана.
SC_SIZE | Изменить размер окна.
SC_TASKLIST | Запустить или активировать Планировщик Задач (Windows Task Manager).
SC_VSCROLL | Вертикальная прокрутка.
HCBT_CLICKSKIPPED

Windows вызывает хук WH_CBT с этим кодом при удалении события от мыши из входной очереди потока, в случае, если установлен хук мыши. Windows вызовет системный хук, когда из какой-либо входной очереди будет удалено событие от мыши и в системе установлен либо глобальный, либо локальный хук мыши. Данный код передается только в том случае, если к хуку WH_MOUSE прикреплена фильтрующая функция. Несмотря на свое название, HCBT_CLICKSKIPPED генерируется не только для пропущенных событий от мыши, но и в случае, когда событие от мыши удаляется из системной очереди. Его главное назначение - установить хук WH_JOURNALPLAYBACK в ответ на событие мыши. (За дополнительной информацией обратитесь к секции "WM_QUEUESYNC".)

В wParam передается идентификатор сообщения мыши - например, WM_LBUTTONDOWN или любое из сообщений WM_?BUTTON*. lParam содержит указатель на структуру MOUSEHOOKSTRUCT, которая описана следующим образом:

typedef
struct tagMOUSEHOOKSTRUCT {
    POINT   pt;          // Позиция курсора мыши в координатах экрана
    HWND    hwnd;        // Окно, получающее сообщение
    UINT wHitTestCode;   // Результат проверки координат (hit-testing)
    DWORD   dwExtraInfo; // Доп.информация о сообщении
} MOUSEHOOKSTRUCT, FAR *LPMOUSEHOOKSTRUCT, *PMOUSEHOOKSTRUCT;
HCBT_KEYSKIPPED

Windows вызывает хук WH_CBT с этим кодом при удалении клавиатурного события из системной очереди, в случае, если установлен клавиатурный хук. Windows вызовет системный хук, когда из какой-либо входной очереди будет удалено событие от клавиатуры и в системе установлен либо глобальный, либо локальный клавиатурный хук. Данный код передается только в том случае, если к хуку WH_KEYBOARD прикреплена фильтрующая функция. Несмотря на свое название, HCBT_KEYSKIPPED генерируется не только для пропущенных клавиатурных событий, но и в случае, когда клавиатурное событие удаляется из системной очереди. Его главное назначение - установить хук WH_JOURNALPLAYBACK в ответ на клавиатурное событие. (За дополнительной информацией обратитесь к секции "WM_QUEUESYNC".)

В wParam передается виртуальный код клавиши - то же самое значение, что и в wParam функций GetMessage или PeekMessage для сообщений WM_KEY*. lParam содержит то же значение, что и lParam функций GetMessage или PeekMessage для сообщений WM_KEY*.

WM_QUEUESYNC

Часто приложение интерактивного обучения (Computer Based Training application или CBT-приложение) должно реагировать на события в процессе, для которого оно разработано. Обычно такими событиями являются события от клавиатуры или мыши. К примеру, пользователь нажимает на кнопку OK в диалоговом окне, после чего CBT-приложение желает послать главному приложению серию клавиатурных нажатий. CBT-приложение может использовать хук мыши для определения момента нажатия кнопки OK. После этого, CBT-приложение должно выждать некоторое время, пока главное приложение не закончит обработку нажатия кнопки OK (CBT-приложение вряд ли хочет послать клавиатурные нажатия диалоговому окну).

CBT-приложение может использовать сообщение WM_QUEUESYNC для определения момента окончания нужного действия. Слежение производится с помощью клавиатурного или мышиного хуков. Наблюдая за главным приложением с помощью хуков, CBT-приложение узнает о наступлении необходимого события. После этого CBT-приложение должно подождать окончания этого события, прежде чем приступать к выполнению ответных действий.

Для определения момента окончания обработки события, CBT-приложение делает следующее:

1. Ждет от Windows вызова хука WH_CBT с кодом HCBT_CLICKSKIPPED или HCBT_KEYSKIPPED. Это происходит при удалении из системной очереди события, которое приводит к срабатыванию обработчика в главном приложении.

2. Устанавливает хук WH_JOURNALPLAYBACK. CBT-приложение не может установить этот хук, пока не получит код HCBT_CLICKSKIPPED или HCBT_KEYSKIPPED. Хук WH_JOURNALPLAYBACK посылает CBT-приложению сообщение WM_QUEUESYNC. Когда CBT-приложение получает такое сообщение, оно может выполнить необходимые действия, например, послать главному приложению серию клавиатурных нажатий.

HCBT_SETFOCUS

Windows вызывает хук WH_CBT с таким кодом, когда Windows собирается передать фокус ввода какому-либо окну. Когда хук установлен как локальный, это окно должно принадлежать потоку, на который установлен хук. Если фильтр вернет TRUE, фокус ввода не изменится.

В wParam передается хэндл окна, получающего фокус ввода. lParam содержит хэндл окна, теряющего фокус ввода.

HCBT_QS

Windows вызывает хук WH_CBT с этим кодом когда из системной очереди удаляется сообщение WM_QUEUESYNC, в то время как происходит изменение размеров или перемещение окна. Ни в каком другом случае этот хук не вызывается. Если хук установлен как локальный, это окно должно принадлежать потоку, на который установлен хук.

Оба параметра - и wParam, и lParam - содержат ноль.

WH_DEBUG

Windows вызывает этот хук перед вызовом какой-либо фильтрующей функции. Фильтры не могут изменять значения, переданные этому хуку, но могут предотвратить вызов фильтрующей функции, возвратив ненулевое значение.

В wParam передается идентификатор вызываемого хука, например, WH_MOUSE. lParam содержит указатель на следующую структуру:

typedef
struct tagDEBUGHOOKINFO
{
    DWORD   idThread;  // Идентификатор текущего потока
    LPARAM  reserved;
    LPARAM  lParam;    // lParam для фильтрующей функции
    WPARAM  wParam;    // wParam для фильтрующей функции
    int     code;
} DEBUGHOOKINFO, *PDEBUGHOOKINFO, NEAR *NPDEBUGHOOKINFO, FAR* LPDEBUGHOOKINFO;
WH_FOREGROUNDIDLE

Windows вызывает этот хук, когда к текущему потоку не поступает пользовательский ввод для обработки. Когда хук установлен как локальный, Windows вызывает его только при условии отсутствия пользовательского ввода у потока, к которому прикреплен хук. Данный хук является уведомительным, оба параметра - и wParam, и lParam - равны нулю.

WH_GETMESSAGE

Windows вызывает этот хук перед выходом из функций GetMessage и PeekMessage. Фильтрующие функции получают указатель на структуру с сообщением, которое затем (вместе со всеми изменениями) посылается приложению, вызвавшему GetMessage или PeekMessage. В lParam находится указатель на структуру MSG:

typedef
struct tagMSG {     /* msg */
    HWND   hwnd;     // Окно, чья Winproc получит сообщение
    UINT   message;  // Номер сообщения
    WPARAM wParam;
    LPARAM lParam;
    DWORD  time;     // Время посылки сообщения
    POINT  pt;       // Позиция указателя мыши (в экранных координатах)
    //  для этого сообщения
} MSG;
WH_HARDWARE

Этот хук в Win32 пока не реализован.

Регистрационные хуки

Регистрационные хуки (journal hooks) используются для записи и воспроизведения событий. Они могут устанавливаться только как системные, и, следовательно, должны использоваться как можно реже. Эти хуки воздействуют на все приложения Windows; хотя десктоп и не позволяет такого другим хукам, регистрационные хуки могут записывать и воспроизводить последовательности событий и от десктопа, и для десктопа. Другой побочный эффект регистрационных хуков в том, что все системные входные очереди проходят через один поток, который установил такой хук.

В Win32 предусмотрена специальная последовательность действий, с помощью которой пользователь может убрать регистрационный хук (например, в случае, если он завесил систему). Windows отключит записывающий или воспроизводящий регистрационный хук, когда пользователь нажмет CTRL+ESC, ALT+ESC, или CTRL+ALT+DEL. Windows оповестит приложение, установившее этот хук, посылкой ему сообщения WM_CANCELJOURNAL.

WM_CANCELJOURNAL

Это сообщение посылается с хэндлом окна, равным NULL, чтобы оно не попало в оконную процедуру. Лучший способ получить это сообщение - прикрепить к WH_GETMESSAGE фильтрующую функцию, которая бы следила за входящими сообщениями. В документация по Win32 упоминается, что приложение может получить сообщение WM_CANCELJOURNAL между вызовами функций GetMessage (или PeekMessage) и DispatchMessage. Хотя это и так, нет гарантий, что приложение будет вызывать эти функции, когда будет послано сообщение. Например, если приложение занято показом диалогового окна, главный цикл обработки сообщений не получит управление.

Комбинации клавиш CTRL+ESC, ALT+ESC, и CTRL+ALT+DEL встроены в систему, чтобы пользователь всегда смог остановить регистрационный хук. Было бы неплохо, если каждое приложение, использующее регистрационные хуки, также предусматривало для пользователя способ остановки тотальной регистрации. Рекомендуемый способ - использовать код VK_CANCEL (CTRL+BREAK).

WH_JOURNALRECORD

Windows вызывает этот хук при удалении события из системной очереди. Таким образом, фильтры этого хука вызываются для всех мышиных и клавиатурных событий, кроме тех, которые проигрываются регистрационным хуком на воспроизведение. Фильтрующие функции могут обработать сообщение (то есть, записать или сохранить событие в памяти, на диске, или и там, и там), но не могут изменять или отменять его. Фильтры этого хука могут находиться и внутри DLL, и в .EXE-файле. В Win32 для этого хука реализован только код HC_ACTION.

HC_ACTION

Windows вызывает хук WH_JOURNALRECORD с этим кодом при удалении события из системной очереди. Этот код сигнализирует фильтрующей функции о том, что это событие является нормальным. В lParam при этом передается указатель на структуру EVENTMSG. Обычная процедура записи состоит в сохранении всех пришедших хуку структур EVENTMSG в памяти или на диске.

Структура EVENTMSG описана в WINDOWS.H следующим образом:

typedef
struct tagEVENTMSG {
    UINT    message;
    UINT    paramL;
    UINT    paramH;
    DWORD    time;
    HWND     hwnd;
} EVENTMSG;

typedefstruct tagEVENTMSG *PEVENTMSG, NEAR *NPEVENTMSG, FAR *LPEVENTMSG;

Элемент message является идентификатором сообщения, одним из значений WM_*. Значения paramL и paramH зависят от источника события - мышь это или клавиатура. Если это событие мыши, в paramL и paramH передаются координаты x и y события. Если это клавиатурное событие, в paramL находятся два значения: скан-код клавиши в HIBYTE и виртуальный код клавиши в LOBYTE, а paramH содержит число повторений. 15-й бит числа повторений служит индикатором дополнительной клавиши. В элементе time хранится системное время (наступления события), которое возвращается функцией GetTickCount. hwnd - это хэндл окна, получившего событие.

Промежуток времени между событиями определяется сравнением элементов time этого события с элементом time последующего события. Разница во времени нужна для корректного проигрывания записанных событий.

WH_JOURNALPLAYBACK

Этот хук используется для посылки Windows клавиатурных и мышиных сообщений таким образом, как будто они проходят через системную очередь. Основное назначение этого хука - проигрывание событий, записанных с помощью хука WH_JOURNALRECORD, но его можно также с успехом использовать для посылки сообщений другим приложениям. Когда к этому хуку прикреплены фильтрующие функции, Windows вызывает первый фильтр в цепочке, чтобы получить событие. Windows игнорирует движения мыши, пока в системе установлен хук WH_JOURNALPLAYBACK. Все остальные события от клавиатуры и мыши сохраняется до тех пор, пока у хука WH_JOURNALPLAYBACK не останется функций-фильтров. Фильтры для этого хука могут располагаться как в DLL, так и в .EXE-файле. Фильтры этого хука должны знать о существовании следующих кодов:

  • HC_GETNEXT
  • HC_SKIP
HC_GETNEXT

Windows вызывает WH_JOURNALPLAYBACK с этим кодом, когда получает доступ к входной очереди потока. В большинстве случаев Windows посылает этот код несколько раз для одного и того же сообщения. В lParam фильтру передается указатель на структуру EVENTMSG (см. выше). Фильтрующая функция должна занести в эту структуру код сообщения message, paramL, и paramH. Обычно эти значения копируются из структур, записанных ранее с помощью хука WH_JOURNALRECORD.

Фильтрующая функция должна сообщить Windows когда нужно начинать обработку посланного сообщения. Windows необходимо для этого два значения: (1) период времени, на которое Windows должно задержать обработку сообщения; либо (2) точное время, когда это сообщение должно быть обработано. Обычно время ожидания обработки вычисляется как разница элементов time структуры EVENTMSG предыдущего сообщения и элемента time той же структуры текущего сообщения. Такой прием позволяет проигрывать сообщения на той же скорости, на которой они были записаны. Если сообщение необходимо проиграть немедленно, функция должна вернуть значение периода времен, равное нулю.

Точное значение времени, в которое нужно обработать сообщение, обычно вычисляется сложением времени, которое Windows должна подождать до начала обработки сообщения и текущего системного времени, получаемого функцией GetTickCount. Для немедленного проигрывания сообщения используйте значение, возвращаемое функцией GetTickCount.

Если система не находится в активном состоянии, Windows использует значения, переданные фильтром, для обработки события. Если система находится в активном состоянии, Windows проверяет системную очередь. Каждый раз, когда она это делает, Windows запрашивает то же самое событие с кодом HC_GETNEXT. Каждый раз, когда функция-фильтр получает код HC_GETNEXT, она должна вернуть новое значение времени ожидания, принимая во внимание время, прошедшее между вызовами функций. Элементы message, paramH и paramL, скорее всего, не потребуют изменений между вызовами.

HC_SKIP

Windows вызывает хук WH_JOURNALPLAYBACK после окончания обработки сообщения, полученного от WH_JOURNALPLAYBACK. Это происходит в момент мнимого удаления события из системной очереди (мнимой, так как событие не находилось в системной очереди, а было сгенерировано хуком WH_JOURNALPLAYBACK). Этот код хука сигнализирует фильтрующей функции о том, что событие, возвращенное фильтром во время вызова предыдущего HC_GETNEXT, попало в приложение. Фильтрующая функция должна приготовиться вернуть следующее событие по приходу кода HC_GETEVENT. Когда фильтрующая функция определяет, что больше нечего проигрывать, она должна удалиться из цепочки фильтров хука во время обработки кода HC_SKIP.

WH_KEYBOARD

Windows вызывает этот хук когда функции GetMessage или PeekMessage собираются вернуть сообщения WM_KEYUP, WM_KEYDOWN, WM_SYSKEYUP, WM_SYSKEYDOWN, или WM_CHAR. Когда хук установлен как локальный, эти сообщения должны поступать из входной очереди потока, к которому прикреплен хук. Фильтрующая функция получает виртуальный код клавиши и состояние клавиатуры на момент вызова клавиатурного хука. Фильтры имеют возможность отменить сообщение. Фильтрующая функция, прикрепленная к этому хуку, должна знать о существовании следующих кодов:

  • HC_ACTION
  • HC_NOREMOVE
HC_ACTION

Windows вызывает хук WH_KEYBOARD с этим кодом при удалении события из системной очереди.

HC_NOREMOVE

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

WH_MOUSE

Windows вызывает этот хук после вызова функций GetMessage или PeekMessage при условии наличия сообщения от мыши. Подобно хуку WH_KEYBOARD фильтрующие функции получают код - индикатор удаления сообщения из очереди (HC_NOREMOVE), идентификатор сообщения мыши и координаты x и y курсора мыши. Фильтры имеют возможность отменить сообщение. Фильтры для этого хука должны находиться в DLL.

WH_MSGFILTER

Windows вызывает этот хук, когда диалоговое окно, информационное окно, полоса прокрутки или меню получают сообщение, либо когда пользователь нажимает комбинацию клавиш ALT+TAB (или ALT+ESC) при активном приложении, установившем этот хук. Данный хук устанавливается для конкретного потока, поэтому его безопасно размещать как в самом приложении, так и в DLL. Фильтрующая функция этого хука получает следующие коды:

  • MSGF_DIALOGBOX: Сообщение предназначено либо диалоговому, либо информационному окну.
  • MSGF_MENU: Сообщение предназначено меню.
  • MSGF_SCROLLBAR: Сообщение предназначено полосе прокрутки.
  • MSGF_NEXTWINDOW: Происходит переключение фокуса на следующее окно.

В WINUSER.H определено больше MSGF_-кодов, но в настоящее время они не используются хуком WH_MSGFILTER.

В lParam передается указатель на структуру, содержащую информацию о сообщении. Хуки WH_SYSMSGFILTER вызываются перед хуками WH_MSGFILTER. Если какая-нибудь из фильтрующих функций хука WH_SYSMSGFILTER возвратит TRUE, хуки WH_MSGFILTER не будут вызваны.

WH_SHELL

Windows вызывает этот хук при определенных действиях с окнами верхнего уровня - top-level windows (то есть, с окнами, не имеющими владельца). Когда хук установлен как локальный, он вызывается только для окон, принадлежащих потоку, установившему хук. Этот хук является информирующим, поэтому фильтры не могут изменять или отменять событие. В wParam передается хэндл окна; параметр lParam не используется. Для данного хука в WINUSER.H определены три кода:

  • HSHELL_WINDOWCREATED: Windows вызывает хук WH_SHELL с этим кодом при создании окна верхнего уровня. Когда фильтр получает управление, это окно уже создано.
  • HSHELL_WINDOWDESTROYED: Windows вызывает хук WH_SHELL с этим кодом перед удалением окна верхнего уровня.
  • HSHELL_ACTIVATESHELLWINDOW: На данный момент этот код не используется.
WH_SYSMSGFILTER

Этот хук идентичен хуку WH_MSGFILTER за тем исключением, что он имеет системную область видимости. Windows вызывает этот хук, когда диалоговое окно, информационное окно, полоса прокрутки или меню получает сообщение, либо когда пользователь нажимает комбинации клавиш ALT+TAB или ALT+ESC. Фильтр получает те же коды, что и фильтры хука WH_MSGFILTER.

В lParam передается указатель на структуру, содержащую информацию о сообщении. Хуки WH_SYSMSGFILTER вызываются до хуков WH_MSGFILTER. Если любая из фильтрующих функций хука WH_SYSMSGFILTER вернет TRUE, фильтры хука WH_MSGFILTER не будут вызваны.

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Это все на сегодня. Пока!

Алекс Jenter jenter@rsdn.ru
Красноярск, 2001. Рассылка является частью проекта RSDN.


No.48

Выпуск No. 48 от 1 июля 2001 г.

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

Прежде всего, конечно, хочу попросить прощения на трехнедельный перерыв. Защита выпускной работы - дело серъезное, особенно если на выполнение этой работы осталась всего пара недель ;) Но я успешно защитился, с чем себя и поздравляю. А так же поздравляю вас, дорогие друзья, поскольку сей факт дал вам возможность вновь лицезреть свежие выпуски любимой рассылки на своих мониторах.

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

/ / / / СТАТЬЯ / / / / / / / / / / / / / / / / / / / / / /

Я никогда не буду использовать MFC

Автор: Dennis Crain
Перевод: Алекс Jenter
Источник: MSDN
Скачать программу-пример METAVIEW (22k)
Почему я?

Программирование всегда доставляло мне удовольствие. То есть, программирование на С. Кто может сказать, что С не может дать человеку все, что нужно для выполнения работы? Мне казалось, что вся эта рекламная шумиха вокруг С++ и MFC - не более чем старание хитрецов из отдела по маркетингу оправдать свою зарплату. Ведь как бы то ни было, а я и во сне мог писать подпрограммы для Windows. Мне нравились изящные, плавные отступы массивных операторов switch, которые могли позаботиться обо всем, и даже больше, что могло прийти в голову любому Windows-приложению. Я был полон решимости не попасть в водоворот. Черная дыра абстракции меня не получит. Нет уж.

Но я почувствовал, что я как будто был один. Создал в интернете группу для тех, кто интересуется С, - и был единственным, кто в нее записался! Все мои коллеги уже носили костюмчики С++ с завязками из MFC для работы. Когда я программировал, мне часто стали говорить: "В MFC это было бы гораздо проще". Но мне не нужно было "проще"! Мне был нужен полный контроль! А комбинация C++ и MFC уводила еще дальше от уже достаточно абстракного мира Windows на С. Меня стала мучить депрессия. Я начал задаваться вопросом о своей компетентности. (Не правда ли, это зашло достаточно далеко?)

Как раз тогда вмешался мой друг Найджел. Он попросил меня написать программу на основе MFC, использующую архитектуру документ/представление. Очень нехотя, я дал ему положительный ответ. И тут же его уточнил, сказав: "И зачем только я хочу заниматься этим скучным делом?"

Ниже следует отчет о процессе написания программы. А точнее, это обсуждение тех областей, где мне пришлось повозиться с некоторыми исследованиями, чтобы получить нужную для приложения функциональность. Можете считать это свидетельством в пользу способности MFC освободить программистов от той рутинной черновой работы, с которой нам приходилось иметь дело еще со времен Windows версии 1.0. Я немного запоздал? Возможно. Но я уверен, что многие из вас тоже задержались в пути.

Постановка задачи

Как я заметил выше, Найджел попросил меня написать программу, использующую архитектуру документ/представление в MFC. Конечной целью этого испытания было мое полное вовлечение в мир MFC. Как гласило техническое задание, это Win32 приложение должно было использовать расширенные метафайлы (enhanced metafiles) в качестве документа (более подробную иноформацию о расширенных метафайлах см. в статье "Enhanced Metafiles in Win32" в MSDN), и выводить документ на экран в различных представлениях: либо в виде изображения, либо в виде заголовка метафайла отображаемого как текст. Кроме того, программа должна была позаботиться о печати вместе с предварительным просмотром. Имея все это в виду, я приступил к работе. Как вы увидите, в процессе пришлось воспользоваться некоторыми искусственными приемами. Хотя они и не были неотъемлемой частью приложения, они помогли реализовать нужную функциональность. Можете считать их полезными советами.

Создание проекта: программа для просмотра метафайлов

Здесь нет ничего сверхсложного. Я просто воспользовался мастером AppWizard Microsoft Visual C++. Я выбрал многодокументный интерфейс (MDI), поставил галочку напротив опции панели инструментов (toolbar), строки состояния (status bar), и предварительного просмотра (print preview). В результирующем проекте были созданы класс документа и представления. Но погодите, я что, сказал "класс представления" вместо "классы представлений"? Почему AppWizard не создал второе представление? Если вы помните, программа должна осуществлять два представления одного документа. Но AppWizard создает только один класс представления. "Вот бездельник", подумал я. Очевидно, мне придется добавить второе представление самому. Ах, ну да, зато предварительный просмотр прилагается бесплатно. Но мы еще вернемся к этому второму представлению документа.

Открытие документа

Хорошо, и где же в такой системе происходит открытие документа? Я неверно предположил, что это будет происходить в ответ на выбор пользователя команды Открыть из меню Файл. Ну и глупец же я. Это было бы слишком очевидно. В течение некоторого времени я шел этой дорогой, пока не понял, что фактически делаю всю работу сам. Я-то думал, что MFC будет мне помогать! "Неважно", подумал я, "просто спущусь в офис к Найджелу и спрошу его". Как оказалось, мне нужно было обрабатывать открытие файла в функции Serialize, принадлежащей классу документа. Следующий код иллюстрирует мою первую попытку сделать это.

void CMetavw1Doc::Serialize(CArchive& ar)
{
  if (ar.IsStoring())
  {
  }
  else
  {

      UINT uiSig;

      ar.Read(&uiSize, sizeof(UINT));

      if (uiSiz == EMR_HEADER)
      {
         m_hemf = GetEnhMetaFile(m_szPathName);
      }

  }
}

Интересующий нас код находится в блоке else. Этот код просто читает первые sizeof(UINT) байт из файла, чтобы убедиться что они являются сигнатурой расширенного метафайла. Если это так, данные загружаются с помощью GetEnhMetaFile. Так как я пока не сохраняю никаких документов, в ответ на IsStoring нет никакого кода.

А не правда ли, было бы неплохо просто вызвать что-то вроде Load(m_szPathName) чтобы загрузить файл? Ага! Эта будет наш первый хитрый прием! Я решил написать класс, который будет иметь дело непосредственно с метафайлами, загружать их и воспроизводить (об этом подробнее см. дальше). Использование этого класса уменьшило необходимый код загрузки до следующего:

void CMetavw1Doc::Serialize(CArchive& ar)
{
  if (ar.IsStoring())
  {
  }
  else
  {
     cemf.Load(m_szPathName);
  }
}

Заметьте, что в обоих фрагментах я использовал переменную m_szPathName как аргумент при вызове функций GetEnhMetaFile и Load. Поначалу я думал, что можно получить полное имя файла из параметра типа CArchive функции Serialize. Ведь CArchive содержит переменную-член m_pDocument, которая указывает на сериализуемый в данный момент объект типа CDocument. Отлично, у CDocument есть очень удобная переменная-член, которая выглядела как раз как то, что мне было нужно: m_strPathName. К сожалению, m_pDocument->strPathName инициализируется нулем при открытии файла. Так что я решил получить имя файла и путь к нему перекрыв фукцию OnOpenDocument. Путь напрямую передавался в OnOpenDocument, так что я просто сделал копию внутри класса CMetavw1Doc в той самой переменной, которая передавалась в качестве параметра функциям GetEnhMetaFile и Load.

BOOL CMetavw1Doc::OnOpenDocument(LPCTSTR lpszPathName)
{
  m_szPathName = lpszPathName;

  if (!CDocument::OnOpenDocument(lpszPathName))
    return FALSE;
  return TRUE;
}

Итак, что я получил практически ничего не делая? На этот вопрос легко ответить. Все, что перечислено в следующем списке (плюс многое другое, что я еще просто не успел оценить, я уверен) было предоставлено MFC:

  • Стандартное диалоговое окно открытия файла
  • Список недавно открытых файлов в меню Файл
  • Возможность перетаскивать файлы из Проводника в мое приложение (они даже открываются!)
  • Невообразимое ощущение легкости.
Прием ?1: Класс расширенного метафайла: CEMF

После заполнения моего класса представления (METAVVW.CPP) кодом, необходимым для успешной отрисовки расширенного метафайла, мне стало ясно что я возвращаюсь к своим старым привычкам неорганизованного кодирования на C. Так что я решил убрать весь этот код из класса представления и создать класс, который будет заниматься загрузкой и воспроизведением метафайлов.

Для целей моего маленького приложения, Load и Draw были самыми важными функциями этого класса. Полностью в духе С++, я также написал несколько дополнительных функций для доступа к различным атрибутам метафайла, таким как дескриптор (handle), строка описания, и указатель на заголовок. Следующий код (взятый из CEMF.H) дает хорошее представление о том, что я сделал из этого класса. Заметьте, что я унаследовал класс от CObject, а не от CDC или CMetaFileDC. CDC включает в себя функции PlayMetaFile и AddMetaFileComment, и в ретроспективе, возможно, было бы более удобно унаследовать класс от CDC. Наследование от CMetaFileDC казалось неправильным, потому что я не создавал метафайлы, а просто просматривал уже существующие. Тем не менее, полностью функциональный класс метафайла мог бы быть унаследован и от CMetaFileDC. Да, есть много способов содрать с кота шкуру (прошу прошения у любителей кошек!)

class CEMF : public CObject
{
// Операции
public:
  CEMF();
  ~CEMF();
  BOOL Load(const char *szFileName);
  BOOL Draw(CDC* pDC, RECT* pRect);
  LPENHMETAHEADER GetEMFHeader(){ return ((m_pEMFHdr) ? m_pEMFHdr : NULL);};
  LPTSTR GetEMFDescString(){ return ((m_pDescStr) ? m_pDescStr : NULL);};
  HENHMETAFILE GetEMFHandle(){ return ((m_hemf) ? m_hemf : NULL);};

protected:
  BOOL GetEMFCoolStuff();
  BOOL LoadPalette();

// Данные
protected:
  CString m_szPathName;
  HENHMETAFILE m_hemf;
  LPENHMETAHEADER m_pEMFHdr;
  LPTSTR m_pDescStr;
  LPPALETTEENTRY m_pPal;
  UINT m_palNumEntries;
  LPLOGPALETTE m_pLogPal;
  LOGPALETTE m_LogPal;
  HPALETTE m_hPal;
};

Функция Load подозрительно смотрит на начало файла, как и мой предыдущий код в функции Serialize. Но теперь нет объекта типа CArchive со всеми его преимуществами. Нет проблем. Использование объекта типа CFile позволяет прочитать сигнатуру. Функции GetEMFCoolStuff и LoadPalette взяты из моей программы-примера EMFDCODE в MSDN. Они получают копии заголовка метафайла, строки описания, и палитры внедренной в метафайл. Конечно, они теперь находятся в классе CEMF.

BOOL CEMF::Load(const char *szFileName)
{
  UINT uiSig;

  // Сохранить имя файла.
  m_szPathName = szFileName;

  // Проверить сигнатуру
  CFile cfEMF;
  cfEMF.Open(m_szPathName, CFile::modeRead | CFile::shareDenyWrite);
  cfEMF.Read(&uiSig, sizeof(UINT));
  cfEMF.Close();

  // Если это EMF, получить его дескриптор.
  if (uiSig == EMR_HEADER)
  {
    m_hemf = GetEnhMetaFile(m_szPathName);
    GetEMFCoolStuff();
    LoadPalette();
  }
  else
    m_hemf = NULL;

  // Возвращаем результат.
  return ((m_hemf) ? TRUE : FALSE);
}

Функция Draw вызывается из функции OnDraw класса представления. Особо интересного там ничего не происходит. Если есть палитра, на что указывает не равное NULL значение переменной-члена m_hPal, палитра выбирается в контекст устройства (DC). Я был сильно озадачен, когда узнал что CDC::SelectPalette требует указатель на объект типа CPalette. Но я был так же заинтригован, когда вдруг обнаружил функцию CPalette::FromHandle. Я мог легко преобразовать дескриптор палитры в объект типа CPalette. Далее это было уже просто делом воспроизведения метафайла с помощью CDC::PlayMetaFile.

BOOL CEMF::Draw(CDC *pdc, RECT *pRect)
{
  ASSERT(m_hemf);

  BOOL fRet = FALSE;
  CRect crect;
  CPalette cpalOld = NULL;

  if (m_hemf)
  {
    if (m_hPal)
    {
      CPalette cpal;
      if ((cpalOld = pdc->SelectPalette(cpal.FromHandle(m_hPal), FALSE)))
        pdc->RealizePalette();
    }
    fRet = pdc->PlayMetaFile(m_hemf, pRect);
    if (cpalOld)
      pdc->SelectPalette(cpalOld, FALSE);
  }
  return (fRet);
}

Что же еще находится в классе CEMF? Как я упоминал выше, есть две закрытых (private) функции, которые управляются с палитрой и заголовком. Я их не буду обсуждать в этой статье, кому интересно см. статьи "Enhanced Metafiles in Win32" и "EMFDCODE.EXE: An Enhanced Metafile Decoding Utility", обе доступны в MSDN. Конечно, вы вероятно захотите также взглянуть на файлы CEMF.CPP и CEMF.H программы METAVIEW, приложенной к статье! Если вы захотите сделать из этого класса что-либо серьезное, предлагаю добавить следующую дополнительную функциональность: возможность делать нумерованые последовательности метафайлов и работать с метафайлами, содержащимися в буфере обмена. Опять же, эти темы описываются в вышеупомянутых статьях.

Отображение документа

Если я понял Найджела правильно, задача состояла в том, чтобы отображать документ тремя различными способами. Во-первых, как картинку в дочернем окне; потом, в виде текста описывающего заголовок метафайла (тоже в дочернем окне), и, наконец, как картинку в окне предварительного просмотра. К тому же, два представления в дочерних окнах нужно было реализовать через архитектуру многодокументного интерфейса (MDI) предоставленную MFC. Рассмотрим каждую из этих задач по отдельности.

Вывод изображения в дочернем окне

Здесь никаких проблем. Надо просто вызвать функцию-член Draw из класса CEMF. Посмотрите поближе на функцию OnDraw в файле METAVVW.CPP.

void CMetavw1View::OnDraw(CDC* pDC)
{
   CMetavw1Doc* pDoc = GetDocument();

  // Флаг для предотвращения рисования во время
  // изменения размера окна, см. OnSize() и FullDragOn() в этом модуле.
  if (m_fDraw)
   {
   // Если мы печатаем или находимся в режиме предварительного
   // просмотра, рабочий прямоугольник узнается из CPrintInfo в OnPreparePrinting
   if (pDC->IsPrinting())
     {
       pDoc->m_cemf.Draw(pDC, &m_rectDraw);
     }
     else
     {
       GetClientRect(&m_rectDraw);
       pDoc->m_cemf.Draw(pDC, &m_rectDraw);
     }
   }
}

Этот код организован вокруг двух условных операторов о которых стоит сказать отдельно. Первое условие является тестом типа "все-или-ничего". Если m_fDraw равно FALSE, не делается никакой попытки что-то нарисовать. Так что же означает m_fDraw? Ну, это мой хитрый прием ?2, и я скоро к нему вернусь. Второе условие проверяет ведется ли отрисовка на принтер (или предварительный просмотр) или в дочернее окно. Член-функция IsPrinting класса CDC - это встроенная (inline) функция, которая возвращает public-переменную CDC::m_bPrinting. Раньше, прежде чем воспользоваться этой функцией, я проверял m_bPrinting напрямую. Когда я обнаружил функцию IsPrinting, это меня озадачило. Ведь эта функция просто возвращала значение m_bPrinting и все. Но похоже это больше в духе C++. Если название переменной m_bPrinting в будущем изменилось бы, мой код перестал бы работать. Но это все еще меня немного беспокоит. Как-никак, а я бывал достаточно сообразителен, чтобы при необходимости залезать в отладчик, прослеживать за несколькими переменными и затем придумывать способ получения желаемого результата. И это приводит меня к моей первой (и возможно последней) гипотезе: инкапсуляция и сокрытие данных могут мстить за чрезмерный энтузиазм.

Ладно, хватит о гипотезах. Вернемся к программе. Мы обсуждаем отрисовку документа в дочернем окне. Клиентскую область окна мы получили с помощью GetClientRect и поместили в m_rectDraw.

if (pDC->IsPrinting())

     {
       pDoc->m_cemf.Draw(pDC, &m_rectDraw);
     }
     else

     {

       GetClientRect(&m_rectDraw);

       pDoc->m_cemf.Draw(pDC, &m_rectDraw);

     }

Затем вызывается функция Draw и пуф! Появляется картинка.

Вывод изображения в окно предварительного просмотра и на принтер

Да, я схитрил. Я здесь сразу буду заниматься двумя представлениями, в окне предварительного просмотра и на принтере. Но я это делаю исключетельно потому, что для MFC между ними очень мало разницы.

Если IsPrinting возвращает TRUE, вызывается функция Draw с тем, что поначалу кажется неинициализированным m_rectDraw.

if (pDC->IsPrinting())

     {

       pDoc->m_cemf.Draw(pDC, &m_rectDraw);

     }

     else
     {
       GetClientRect(&m_rectDraw);
       pDoc->m_cemf.Draw(pDC, &m_rectDraw);
     }

К счастью, это все-таки не так. Библиотека опять приходит на помощь. Когда мы печатаем или находимся в режиме предварительного просмотра, несколько функций вызываются до OnDraw. Эти функции можно перекрыть своими. В данном случае я перекрыл функцию OnPrint (которая в конце концов вызывает OnDraw). В эту функцию передается указатель на объект типа СPrintInfo. Один из переменных-членов этого класса - объект типа CRect, определяющий доступную для печати область. Этот прямоугольник просто копируется в m_rectDraw до вызова OnDraw.

void CMetavw1View::OnPrint(CDC* pDC, CPrintInfo* pInfo)
{
  m_rectDraw = pInfo->m_rectDraw;
  OnDraw(pDC);
}

Остальное все уже достояние истории. Вызывайте Draw и дело сделано!

Одно замечание касательно предварительного просмотра. Я потратил немного времени, пытаясь выянить размеры "страницы" предварительного просмотра. Почесывал задумчиво голову, пытаясь понять, как мне получить координаты точки отсчета и размеры центрированной в окне "страницы". Я даже дошел до того, что сделал копию объекта CPreviewDC (закрытого объекта AFX) просто чтобы узнать координаты точки отсчета. Но мне все равно никак не удавалось получить размеры. Слава богу, я вспомнил свою гипотезу-теперь-ставшую-аксиомой: инкапсуляция и сокрытие данных могут мстить за чрезмерный энтузиазм. Так что немного поворчав по поводу MFC, я наконец понял, что все масштабирование в окне предварительного просмотра будет осуществляться автоматически. А наблюдая при изменении размера за CPrintInfo::m_rectDraw, я заметил что он все время остается одинаковым. Точно как я хотел! Еще одно очко в пользу MFC.

Что может быть легче?

Я не знаю насчет вас, но мой код, отвечающий за рисование, обычно достаточно объемен. И я действительно впечатлен предварительным просмотром, обычным рисованием и печатью, умещенными в 19 строк кода (включая комментарии). Правда, мне пришлось написать класс CEMF. Но послушайте, я ведь могу теперь использовать его в своих будущих программах! И да, библиотека MFC взяла на себя предварительный просмотр и печать. Но знаете что? Пусть Microsoft занимается сопровождением этого кода вместо меня!

ПРИЕМ ?2: РАЗБИРАЕМСЯ С ПОКАЗОМ СОДЕРЖИМОГО ОКНА ПРИ ПЕРЕТАСКИВАНИИ

Так что это был за флаг m_fDraw в функции OnDraw? Вспомните, что он связан в тестом типа "все-или-ничего". Если m_fDraw равняется FALSE, не делается никакой попытки что-либо нарисовать. Я придумал этот прием когда начал отображать большие метафайлы в клиентской области в ответ на изменение размера окна. В системе есть опция (которая устанавлявается в окне Экран (Desktop) панели управления), которая разрешает показывать содержимое окна при его перетаскивании и изменении размера. И я хочу вам сказать, что если вы воспроизводите огромный метафайл, который делает много сложных вещей, вы эту опцию просто возненавидите. Так как ее отключить? Нет, этого делать нельзя! Помните, эту опцию включил пользователь. Вам не стоит самостоятельно отключать ее. Вы можете сказать, "раньше это меня никогда не останавливало!". Ну, все равно удобного способа отключить ее нет. Чтобы справиться с ней, я решил воспользоваться "одноразовым" таймером.

Что такое одноразовый таймер? Просто говоря, это таймер который используется один раз, а потом уничтожается. Основная реализация, в случае изменения размера окна, - запустить таймер при получении сообщения WM_SIZE. Если таймер уже существует (в случае последовательных сообщений WM_SIZE), уничтожить его и запустить еще один. Когда наконец сообщение WM_TIMER пробивается скозь очередь других сообщений, уничтожаем таймер. Помните, что вы не получите сообщения WM_TIMER, пока не прекратится изменение окна приложения. У сообщений WM_TIMER очень низкий приоритет. Следующие две функции, OnSize и OnTimer, иллюстрируют как я приспособил таймер для этого приложения. В дополнение к созданию и уничтожению таймеров, эти функции устанавливают значение m_fDraw.

void CMetavw1View::OnSize(UINT nType, int cx, int cy)
{
  CView::OnSize(nType, cx, cy);
  // Это нужно делать только если опция отображения  содержимого
  // окна при перемещении и изменении размера включена
  if (m_fFullDragOn)
  {
    if (!m_uiTimer)
      KillTimer(1);
    m_uiTimer = SetTimer(1, 100, NULL);
    m_fDraw = FALSE;
  }
}

Когда мы наконец получаем сообщение WM_TIMER, функция OnTimer устанавливает значение m_fDraw в TRUE, уничтожает таймер и перерисовывает клиентскую область окна представления.

void CMetavw1View::OnTimer(UINT nIDEvent)
{
  m_fDraw = TRUE;
  m_uiTimer = 0;
  KillTimer(1);
  InvalidateRect(NULL);
}

Переменная m_fFullDragOn, встречавшаяся в OnSize, устанавливается вызовом функции FullDragOn в конструкторе класса представления. Эта функция смотрит в ключе HKEY_CURRENT_USER\Control Panel\Desktop реестра, включена ли опция показа содержимого окна. Если значение подключа DragFullWindows равно 1, функция возвращает TRUE, иначе она возвращает FALSE. [Расположение ключей в реестре сильно зависит от типа и версии системы. Используйте эту возможность с осторожностью. - прим. перев.]

BOOL CMetavw1View::FullDragOn()
{
  HKEY hkey = NULL;
  DWORD dwType;
  long lResult;
  LPSTR lpszDataBuf;
  DWORD cbData = 0;

  lResult = RegOpenKeyEx(HKEY_CURRENT_USER, "Control Panel\\Desktop", 0,
                            KEY_READ, &hkey);
  if (hkey)
  {
    // Получить размер ключа.
    lResult = RegQueryValueEx(hkey, "DragFullWindows", NULL,
                              &dwType, NULL, &cbData);
    // Зарезервировать память под значение ключа.
    lpszDataBuf = (LPSTR)malloc(cbData * sizeof(char));
    // Получить значение ключа.
    lResult = RegQueryValueEx(hkey, "DragFullWindows", NULL, &dwType,
                             (LPBYTE)lpszDataBuf, &cbData);
    return (*lpszDataBuf == '1');
  }
  return FALSE;
}

Общий результат таков: когда окно изменяет размер, ничего не перерисовывается до тех пор, пока изменение размера не прекратится.

Добавление второго представления

Вы наверное помните, что в задаче Найджела было требование, что программа должна выводить документ на экран в различных представлениях: либо в виде изображения, либо в виде заголовка метафайла отображаемого как текст. Никаких проблем; наверняка для этого есть свой мастер наподобие AppWizard. К моему сильнейшему изумлению, никакого мастера не было! Так что я посмотрел статью Дейла Роджерсона (Dale Rogerson) "Multiple Views for a Single Document" ("Несколько представлений для одного документа") в MSDN. Она была очень полезной. Тем не менее, пока вы не проделаете это три или четыре раза, вы очень просто можете запутаться! Поверьте мне, я потерял несколько часов, когда брался то за добавление второго представления, то за написание класса CEMF. Я советую полностью сфокусироваться на втором представлении, пока оно не заработает как надо. Найджел добавил второе представление в одну из своих программ-примеров ("VIEWDIB: Views Multiple DIBs Simultaneously" Прим. редактора: к сожалению, это приложение больше не входит в состав библиотеки MSDN), основанную на статье Дейла. Он вывел следующий список, основанный на своем опыте. Имея статью Дейла и список Найджела, я смог добавить второе представление без особых хлопот. А если я могу это сделать, то вы тоже можете!

  1. Воспользуйтесь ClassWizard чтобы создать новый класс представления; например, CAppSecondView.
  2. Добавьте включение заголовочного класса нового представления в нужный cpp-файл (см. пункт 12).
  3. ДОбавьте функцию GetDocument к коду класса представления и заголовку. (Скопируйте ее из другого класса представления.)
  4. Напишите код функции OnDraw для нового представления, или по крайней мере поставьте простую заглушку, которая вам позволит его протестировать. Убедитесь, что код класса нового представления включен в проект, и откомпилируйте новый модуль.
  5. Создайте новый ресурсный идентификатор, типа IDR_VIEW2TYPE. Этот идентификатор будет использоваться для всех ресурсов второго представления.
  6. Создайте пиктограмму для нового представления, используйте ресурсный идентификатор из пункта 5.
  7. Создайте меню для второго представления (можете скопировать старое), опять используйте ресурсный идентификатор из пункта 5.
  8. Добавьте в каждое меню пункты для смены текущего представления.
  9. В строковую таблицу добавьте новую строку шаблона для нового ресурсного идентификатора в виде:
    \nТип\n\n\n\nТип файла\nТип файла
  10. В класс приложения добавьте public-переменную типа CMultiDocTemplate* для каждого представления, например m_pBasicViewTemplate и m_pNewViewTemplate.
  11. В файл заголовка класса приложения включите следующую строку после объявления класса:
    extern C???App NEAR theApp; (замените ??? на название вашего приложения.)
  12. Добавьте код создания каждого шаблона документа в реализацию класса приложения, напр.:
    m_pBasicViewTemplate = new CMultiDocTemplate(...);
    AddDocTemplate(m_pBasicViewTemplate);
  13. С помощью ClassWizard'a добавьте в класс главного окна обработчики команд меню нового представления. Все обработчики в виде:
    CreateOrActivateFrame(theApp.m_p????ViewTemplate, RUNTIME_CLASS(C???View));
    Также не забудьте включить заголочный файл нового представления в MAINFRM.CPP
  14. Добавьте функцию CreateOrActivateFrame в MAINFRM.CPP и MAINFRM.H.
Заключение
О'кей, это еще одна статья, прославляющая MFC. Что я могу сказать. Я действительно был скептиком до того, как приступил к работе. Если вы еще не решились, я советую вам попробовать. Я помню одного знакомого, который говорил: "Я никогда не прикоснусь к продукту Microsoft, так как они представляют собой все зло индустрии програмного обеспечения." Теперь он работает в Microsoft! Так что никогда не говорите никогда. Это тот подход, который я выбрал в отношении С++ и MFC. И кстати, вы слышали что новая версия Visual C++ будет называться Visual Cobol++? Вот тогда мне наверное придется уйти в отставку!

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Это все на сегодня. Пока!

Алекс Jenter jenter@rsdn.ru
Красноярск, 2001. Рассылка является частью проекта RSDN.


No.49

Выпуск No. 49 от 8 июля 2001 г.

Добрый день, дорогие подписчики!

/ / / / СТАТЬЯ / / / / / / / / / / / / / / / / / / / / / /

Добавление технологии Connection point в приложение на базе библиотеки MFC

Автор: Евгений Щербатов
Часть 1. Необходимость существования и принцип работы Connection point

В данной статье я сделал попытку объяснить, что такое Connection point, её (его - кому как нравится) устройство и принцип работы. Плохо или хорошо у меня это получилось, судить вам.

Структура статьи построена таким образом, чтобы человек, НИЧЕГО не знающий об этой аббревиатуре, не только понял общие принципы работы, но и смог реализовать данную возможность в своих программах с использованием MFC технологии. Я сам в свое время реально столкнулся с проблемой в максимально сжатые сроки изучить и реализовать этого "зверя", ни разу и не слышав о нем раньше. Если вы находитесь в подобной ситуации, то эта статья для вас.

Я не претендую на академическую точность изложения материала, потому как тот кусочек текста, что вы будете читать, есть не детальный перевод материала из MSDN, а моя попытка систематизировать те знания, которыми я обладаю и преподнести их вам в том контексте и порядке, который, как я считаю, сильно помог бы мне в свое время. Считаю нужным заметить, что реализация Connection point на MFC и ATL сильно отличаются, впрочем, равно как и реализация самих СОМ-серверов. Именно ввиду этого я и выбрал в качестве примеров библиотеку MFC. Дело в том, что про использование Connection point на ATL написано немало статей - я в этом лично убедился. Да и, кроме того, сам мастер в ATL без проблем позволяет добавить и использовать эту технологию. Что же касается MFC, то здесь это сопряжено с некоторыми трудностями. Поэтому, думаю, что те люди, у которых будет необходимость работать именно с MFC, хоть немного, но получат пользу от этого материала. Я постараюсь детально рассказать о тех подводных камнях, с которыми вы можете столкнуться при этом, и помогу вам преодолеть их. Остается добавить, что я буду искренне благодарен любым поправкам и советам в мой адрес.

Итак, начнем! Для начала неплохо бы открыть MSDN и посмотреть словарик на этот счет - Glossary (Platform SDK: COM). Мы можем увидеть следующую вещь:

Connection point object (объект точки связи)
Это СОМ-объект, который управляется Connectable object и содержит реализацию IConnectionPoint интерфейса. Одна или более точек соединения объектов может быть создана и управляться Connectable object. Каждая точка соединения объекта управляет поступлением событий от специфического интерфейса к другому объекту и пересылкой этих событий к клиенту.

Теперь смотрим, что такое Connectable object:

Connectable object (соединяемый объект)
Это СОМ-объект, который реализует, как минимум, интерфейс IConnectionPointContainer для управления точкой соединения объектов. Соединяемые объекты поддерживают связь от сервера к клиенту. Соединяемый объект может создавать и управлять одной или более точками соединения подобъектов, которые получают события от интерфейсов реализованных в других объектах и посылают их клиентской стороне.

Ну как, все понятно? Мне не очень: - тогда читаем дальше.

Давайте вспомним знаменитый CALLBACK способ общения интерфейса API программ на языке C. Предположим, что у вас есть некая DLL, которая содержит в себе экспортируемую функцию, предназначенную для архивирования документов. Пусть у неё имеется два параметра. Первый - это путь к папке с документами, которую следует заархивировать, а второй: ну второй - это указатель на функцию. Callback-функцию - функцию обратного вызова.

FolderArchiving( LPCSTR lpszFolderPath, LOGCALLBACKFUNC *pfnLogCallbackFunc)

Где формат функции обратного вызова следующий:

typedef BOOL (LOGCALLBACKFUNC)(LPCSTR lpszDocPath, int nCurrenDoc);

Т.е. в эту функцию будет передаваться номер архивируемого документа и путь к нему.

Как же это все работает? Клиент определяет в своем приложении функцию с любым именем, строго имеющую те же параметры, что описаны выше - речь идет о функции обратного вызова, - а затем передает указатель на неё в FolderArchiving. После чего DLL начинает свою работу по архивированию сообщений и периодически, перед началом упаковки каждого документа будет вызывать ту функцию, адрес которой передал ей клиент, указывая в её параметрах номер документа и путь к нему. Таким образом, клиентское приложение получает весьма симпатичный механизм наблюдения за процессом архивирования. И при желании может вести log-файл, а также отображать диалог прогресса, если таковой не реализован в DLL. Вот, собственно, что такое CALLBACK, если объяснить на пальцах в двух словах. На рисунке 1 вы видите небольшую диаграмму, схематически поясняющую процесс работы, описанный выше.

Рисунок 1

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

Теперь представим ситуацию, что та DLL, о которой шла речь, получила кроме API-интерфейса ещё и СОМ-интерфейс. А в вашу задачу входит реализация всех API-функций в виде СОМ-функций. И что же мы будем делать с нашим Callback? Вот тут на помощь и придет технология Connection point! Это фактически тот же механизм обратного вызова, только приспособленный для СОМ модели, естественно со своими правилами и отличиями.

Модель COM не всегда имела возможность взаимодействия с помощью исходящих интерфейсов. Было время, когда она воспринималась исключительно как модель входящих интерфейсов. В этой связи, чтобы подчеркнуть важность существования Connection points, давайте смоделируем следующую ситуацию.

Пусть у нас имеется приложение PostAgent с СОМ-интерфейсом. Пусть оно будет ЕХЕ-сервером и может работать как самостоятельное приложение с графическим интерфейсом. В число достоинств этой программы входит работа с архивами почтовых программ. Некоторые особенности работы с почтовыми архивами оказались настолько важны, что разработчики PostAgent вынесли их в отдельные СОМ функции в каком-либо интерфейсе. Замечательно! Теперь вы сможете пользоваться услугами этого приложения, и ваша программа получит тот функционал, которого, возможно, вам так не хватало все это время.

Путь среди функций этого СОМ сервера имеется функция MessageArchiving, которая архивирует почтовые сообщения программы Outlook фирмы Майкрософт. Архивы могут быть настолько огромными, что этот процесс подвесит ваше клиентское приложение на несколько часов (без какой-либо возможности взаимодействия с пользователем), пока функция архивирования наконец не выполнится полностью СОМ-сервером. Как можно бороться с этим? Первое, что приходит на ум, - это создание отдельного потока, в котором и вызовется эта "долгая" функция. Но не все тут так просто: Давайте вспомним, что для того, чтобы использовать модель СОМ в вашем приложении, вы должны проинициализировать её с помощью функции CoInitialize или CoInitializeEx. Думаю, первая функция нам не подходит, т. к. наверняка вы будете использовать не только MessageArchiving, но и другие методы. Если учесть, что специально для неё (MessageArchiving) мы создали отдельный поток, то логично предположить, что работа с остальными функциями сервера PostAgent будет осуществляться, как минимум, ещё из основного потока. Поэтому, чтобы из всех потоков можно было работать с одним и тем же СОМ-сервером, нам необходима многопоточная инициализация модели СОМ (MTA), с помощью CoInitializeEx. Доказал? Будем надеяться, что да.

Что дальше? У нас все работает - потоки отлично исполняются, сообщения архивируются, даже налажен четкий механизм критических секций в коде: Но вот ваш шеф сказал вам, что ваше приложение ДОЛЖНО поддерживать механизм OLE-drag-drop. Вы сталкивались с этим? Нет? Значит, ваше счастье. Почему я так говорю? А потому, что для того, чтобы работать с drag-drop, нужно инициализировать однопоточную модель СОМ (STA), и все тут! Дело в том, что пока нет известного мне механизма OLE-drag-drop, реализованного через MTA. Когда разрабатывалась и создавалась технология ОЛЕ, в ней вообще не было понятия MTA.

Ну и что теперь будем делать дальше? Можно попытаться объяснить шефу, что это "невозможно". Но если он не поймет этого? Тогда придется инициализировать СОМ для каждого потока отдельно, в каждом из них вызывать СОМ-сервер, вводить глобальные переменные для того, чтобы обмениваться информацией между разными функциями одного и того же СОМ-сервера. А можно инициализировать STA один раз, но при этом применять процедуры маршалинга и самому следить за синхронизацией потоков и вызовами СОМ. В общем, кошмар. Допускаю, что это несколько утрированный пример, и, возможно, его можно как-то решить способами, которых я не знаю. Однако я сомневаюсь, что эти решения дадутся без боли в голове и будут претендовать на изящность. Ну а если, ко всему прочему, я добавлю ещё и неаккуратность самих разработчиков сервера PostAgent, то вполне может произойти ситуация, когда PostAgent просто повиснет, зациклится: да что угодно, но управления вам так и не вернет. Почему вы должны страдать от этого?

Вполне логично спросить, наконец, чего ради я начал тут столь бурные излияния на искусственно придуманный пример, и какое отношение ко всему этому имеет Connection point. А вот давайте все же немного дофантазируем, раз уж вы смогли дочитать этот текст до настоящего момента и понять, о чем идет речь.

А если с самого начала разработчики PostAgent подошли бы немного с другой точки зрения к реализации своих интерфейсов? Что я имею ввиду? А вот что. Все вызовы СОМ являются по своей природе синхронными вызовами. Это означает, что СОМ-сервер не отдаст вам управление до тех пор, пока не выполнится функция, которую вы вызвали. Но с появлением Connection point получила право на жизнь и асинхронная модель функций в СОМ. То есть "солидные" приложения и их разработчики (не в пример разработчикам PostAgent) реализуют в своих серверах 2 интерфейса для одного и того же набора функций. Один из них является синхронным, а другой - асинхронным. Про некоторые прелести синхронных функций я вам только что рассказал. А как работают асинхронные функции?

Примерно так. Вы вызываете асинхронную функцию, реализация которой заключается в том, чтобы сохранить переданные ей параметры в некой глобальной структуре данных, затем запустить внутри СОМ-сервера поток, который будет выполнять действия по обработке переданных данных и вернуть управление клиенту. Вот и все! И никакой головной боли, что я описывал выше. Вы вызвали MessageArchiving, она запомнила переданные вами данные, вернула вам управление, а сама заставила PostAgent выполнять задачу в фоновом для вас режиме. Однако если идти дальше, то вам может понадобиться и механизм, с помощью которого вы смогли бы не только наблюдать за тем, что делает сейчас работающий СОМ-сервер, но так же и знать когда он закончит свою работу. Ведь так?! Понимаете, куда я клоню? Правильно! Сигнализировать о том, что работа функции, наконец, закончена, может специально предназначенный для этого Connection point. Кроме того, с его же помощью можно и прервать работу функции, если сделать соответствующий функционал в сервере. Но об этом позже. Таким образом, умелое сочетание синхронных и асинхронных функций даст возможность реализовать максимально эффективное взаимодействие через СОМ.

Думаю к настоящему моменту уже должно быть понятно, зачем нужны Connection points , а также хотя бы примерно, что это такое. Теперь перейдем к более детальному рассмотрению.

Connection point подразумевает под собой соединение. Соединение состоит из 2 частей: из объекта, вызывающего интерфейс и называемого источником, и из объекта, содержащего реализацию этого интерфейса и называемого приемником. Устанавливая точку соединения, источник позволяет приемнику присоединиться к себе. С помощью механизма точки соединения (интерфейс IConnectionPoint) указатель на интерфейс приемника передается объекту источника. Этот указатель обеспечивает источнику доступ к функциям-членам реализации приемника. К примеру, для возбуждения события, реализованного приемником, источник может вызвать соответствующий метод из реализации приемника.

Часть 2. Создание СОМ-сервера

Программа-пример Server

Итак, давайте создадим внутризадачный СОМ-сервер (DLL), который будет предоставлять клиенту одну-единственную функцию, результатом работы которой будет возбуждение события. Для этого запустите Visual Studio и выберите меню File->New. Затем в появившемся диалоге выберите вкладку Projects и в поле имени проекта введите ту информацию, что указана на рисунке 2, а также в качестве используемого мастера выберите MFC AppWizard (dll).

Рисунок 2

Нажмите ОК и вы увидите следующий диалог (рисунок 3). Поставьте установки, как показано на рисунке, и не забудьте отметить птичкой опцию Automation. Это важно, т.к. лишь в этом случае мастер добавит к нашему проекту код, необходимый для СОМ-сервера.

Рисунок 3

Нажмите Finish. Если вы сделали все правильно, то в окне ClassView вы должны увидеть ту же картину, что и на рисунке 4.

Рисунок 4

Итак, что же нам создал мастер? Он добавил минимальный джентльменский набор, который обеспечит существование скромному внутризадачному (DLL) СОМ-серверу. Это функции DllCanUnloadNow, DllGetClassObject и DllRegisterServer. Среди этих функций также должна находиться функция DllUnregisterServer, которая, на мой взгляд, также важна, как и DllRegisterServer, однако разработчики в Майкрософт почему-то так не считают. О том, как добавить в СОМ-сервер механизм unregistered, я расскажу в другой статье. Кроме того, в функцию InitInstance мастер добавил следующий код регистрации фабрики классов, за что ему и спасибо.

BOOL CPointServerApp::InitInstance()
{
   // Register all OLE server (factories) as running.  This enables the
// OLE libraries to create objects from other applications.
   COleObjectFactory::RegisterAll();

   return TRUE;
}

Пожалуй, все? Нет, не все. Если вы посмотрите в файлы проекта, созданные мастером, то увидите среди них замечательный файл описания интерфейсов PointServer.odl, содержимое которого имеет вид:

// PointServer.odl : type library source for PointServer.dll
// This file will be processed by the MIDL compiler to produce the
// type library (PointServer.tlb).

[ uuid(D46238B8-2277-11D5-964D-00001CDC1022), version(1.0) ]
library PointServer
{
   importlib("stdole32.tlb");
   importlib("stdole2.tlb");

   //{{AFX_APPEND_ODL}}
//}}AFX_APPEND_ODL}}
};

Что же, так и должно быть - это заготовка библиотеки, в которую мы потом добавим необходимый код. Кстати, учтите, что uuid-идентификаторы у вас будут отличаться, так как это все же уникальное число, поэтому не забывайте далее по коду вставлять свои значения.

Теперь добавим в наш сервер интерфейс, который будет содержать в себе один метод. Для этого выберите View->ClassWizard. В появившемся диалоге выберите вкладку Automation и нажмите Add Class->New. Появится диалог, изображенный на рисунке 5. Введите в необходимые поля информацию, указанную на рисунке.

Рисунок 5

Таким образом, мы указываем мастеру, что хотим добавить в наш сервер объект, обработка событий которого будет происходить в классе CMyInterface, и что ProgId нашего интерфейса будет иметь имя (его потом можно будет использовать для идентификации интерфейса при его вызове, например, в БЕЙСИКе - это очень удобно).

Кроме того, обратите внимание на одну важную особенность! Наш класс является производным от класса CCmdTarget - это необходимо. Дело в том, что библиотека MFC реализует модель Connection point в классах CConnectionPoint и CCmdTarget. Классы, наследуемые от CСonnectionPoint, реализуют IConnectionPoint интерфейс, используемый для предоставления точек соединения другим объектам, а классы, наследуемые от CСmdTarget, реализуют IConnectionPointContainer интерфейс, который может перечислять все доступные точки соединения объекта или искать специфическую точку соединения. Вернитесь к началу статьи и прочитайте ещё раз определения терминов Connectable object и Connection point object.

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

Ну, вот теперь жмите ОК.

Теперь в диалоге MFC ClassWizard, который, я надеюсь, вы ещё не закрыли, вам стала доступна кнопка Add Method. Жмите её и заполняйте предложенную вам форму данными, указанными на рисунке 6.

Рисунок 6

Здесь мы указываем мастеру, что нам необходимо определить в нашем интерфейсе функцию FireMyEvent без параметров. Эту функцию мы будем использовать с единственной целью - чтобы сгенерировать событие, которое вызовет исходящую функцию - собственно наш Connection point. Жмите ОК в этом диалоге, а затем и в диалоге MFC ClassWizard.

Смотрим, что получилось. На рисунке 7 видно, что в наш проект добавились новые классы.

Рисунок 7

Это класс CMyInterface и интерфейс IMyInterface. Я не буду подробно описывать те вещи, которые добавил мастер в ODL файл, а также в файл реализации класса CMyInterface, так как это предмет другого разговора. А мы сейчас пытаемся добавить поддержку точек соединения, и я предполагаю, что с подобными вещами вы уже знакомы.

Теперь нам нужно сгенерировать уникальный GUID. Он будет однозначно идентифицировать интерфейс, который мы собираемся описать в ODL-файле, и который будет содержать исходящий метод, что мы будем вызывать на клиентской стороне. Я для этого пользуюсь замечательной утилитой Guidgen.exe, которая поставляется вместе со студией. Итак, откройте ODL файл нашего проекта и сразу после директив импорта двух TLB-фалов, что любезно добавил туда мастер, вставьте следующее объявление интерфейса, не забывая при этом менять значения уникального идентификатора на свое значение.

importlib("stdole32.tlb");
importlib("stdole2.tlb");

[ uuid(F7222740-2296-11d5-964D-00001CDC1022) ]
dispinterface IFireClassEvents
{
   properties:
   methods:
      [id()] boolean MyEvent();
}//  Primary dispatch interface for CMyInterface

[ uuid(D46238C5-2277-11D5-964D-00001CDC1022) ]
dispinterface IMyInterface
{
   properties:

Как видите, мы объявили метод MyEvent в интерфейсе IFireClassEvents, который и будет являться нашим событием, которое мы будем <посылать> клиентскому приложению. Я оставил этот метод без параметров, а тип возвращаемого результата сделал булевским. Будем считать это маленьким капризом. Вы вольны описать этот метод, как вам будет угодно.

Далее продолжаем редактировать ODL-файл. В описании нашего кокласса MyInterface нужно добавить одну строку:

[ uuid(D46238C6-2277-11D5-964D-00001CDC1022) ]
coclass MyInterface
{
   [default] dispinterface IMyInterface;
   [default,source] interface IFireClassEvents;
};

Замечаем отличия? Конечно! Появилось новое служебное слово source. Что оно означает? В MSDN написано, что <атрибут [source] указывает, что член кокласса, свойство или метод - это источник событий. Для членов кокласса этот атрибут означает, что этот член вызовется раньше, чем выполнится>. А также есть такая фраза: <В свойстве или методе этот атрибут указывает, что член возвращает объект или VARIANT, который является источником событий. Объект реализует интерфейс IConnectionPointContainer>. Думаю, с этим все ясно. Сохраните сделанные изменения и откомпилируйте ODL-файл. Если вы все сделали правильно, то ошибок не будет.

Для каждой точки соединения, реализованной в нашем классе CMyInterface, мы должны объявить connection point part, которая реализует точку соединения. Если вы реализуете одну или более точек соединения вы также должны объявить простую карту соединений в вашем классе. Карта соединений - это таблица точек соединения, поддерживаемых ActiveX-контролом. Наш пример демонстрирует простую карту соединений и одну точку соединения. Для этого откройте файл объявления класса MyInterface.h и сразу после макроса DECLARE_INTERFACE_MAP() добавьте нижеприведенный код:

DECLARE_CONNECTION_MAP()

BEGIN_CONNECTION_PART(CMyInterface,ObjCP)
   CONNECTION_IID(IID_IFireClassEvents)
END_CONNECTION_PART(ObjCP)

Таким образом мы объявляем карту соединений. BEGIN_CONNECTION_PART и END_CONNECTION_PART - это макросы, объявленные во встроенном классе и наследуемые от CConnectionPoint, который реализует эту точку соединения. Если вы хотите переопределить какую-либо функцию класса CConnectionPoint или добавить функцию-член вашего родителя, тогда объявите её между двумя этими макросами. К примеру, макрос CONNECTION_IID, расположенный между двумя этими макросами, переопределяет функцию CConnectionPoint::GetIID.

Я буду немного непоследователен, однако попрошу вас сразу, раз уж вы редактируете данный заголовочный файл, подняться к объявлению класса CMyInterface и перед его объявлением вставить следующие строки:

extern
const IID IID_IFireClassEvents;
interface IFireClassEvents : public IDispatch {};

Первая строка объявляет константу IID_IFireClassEvents, которую мы использовали в макросе CONNECTION_IID(IID_IFireClassEvents). Ведь все должно работать без ошибок. Однако перед описанием этой константы стоит служебное слово extern. Это значит, что она уже где-то объявлена. Поэтому я и сказал, что слегка непоследователен. Здесь все правильно. Сейчас мы закончим с заголовочным файлом и объявим этот идентификатор интерфейса в файле реализации класса CMyInterface.

Вторая строка есть объявление интерфейса. Теперь сохраните файл MyInterface.h и закройте его.

Следующим этапом добавим в файл MyInterface.cpp то, о чем собственно говорили только что выше. Найдите в коде декларацию идентификатора IID_IMyInterface и сразу после него вставьте следующие строки (только не забывайте вставлять правильные числовые значения, если вы используете uuid, отличный от того, что создала мне утилита guidgen.exe):

#pragma warning( disable : 4211)static
const IID IID_IFireClassEvents =
//(F7222740-2296-11d5-964D-00001CDC1022)
{ 0xf7222740, 0x2296, 0x11d5, { 0x96, 0x4d, 0x0, 0x0, 0x1c, 0xdc, 0x10, 0x22 } };
#pragma warning( default : 4211)

Обратите внимание на две директивы препроцессора, которые окаймляют данное объявление. Первая из них гасит ненужное предупреждение компилятора, а вторая снова его разрешает. Теперь добавьте в карту интерфейсов интерфейс IConnectionPointContainer (MyInterface.cpp):

BEGIN_INTERFACE_MAP(CMyInterface, CCmdTarget)
   INTERFACE_PART(CMyInterface, IID_IMyInterface, Dispatch)
   INTERFACE_PART(CMyInterface, IID_IConnectionPointContainer, ConnPtContainer)
END_INTERFACE_MAP()

И наконец, сразу за картой интерфейсов вставьте карту соединений:

BEGIN_CONNECTION_MAP(CMyInterface, CCmdTarget)
   CONNECTION_PART(CMyInterface,IID_IFireClassEvents,ObjCP)
END_CONNECTION_MAP()

Затем добавьте в конструктор нашего класса вызов функции EnableConnections. Эта функция является недокументированной функцией класса CCmdTarget, однако если вы не вызовете её из конструктора класса, работающего с Connection point и являющегося потомком CCmdTarget , то у вас ничего не получится.

CMyInterface::CMyInterface()
{
   EnableAutomation();
   EnableConnections();

   // To keep the application running as long as an OLE automation
//   object is active, the constructor calls AfxOleLockApp.

   AfxOleLockApp();
}

Сохраните редактируемый файл и постройте наш проект. Если вы делали все правильно, то не должно быть ни ошибок, ни предупреждений. На данный момент у нас готов основной каркас. Займемся реализацией непосредственно самих функций. Однако сначала я хотел бы обратить ваше внимание на следующий факт. Я думаю, ни для кого не секрет, что СОМ-сервер может иметь сразу нескольких клиентов. Может случиться так, что этот сервер должен посылать событие клиентам в ответ на какое-либо действие пользователя. Например, сервер имеет графический интерфейс, и когда пользователь выбирает меню Файл, сервер должен уведомить ВСЕХ ЗАИНТЕРЕСОВАННЫХ клиентов об этом. Естественно возникает 2 вопроса:

  1. Каким образом сервер сможет послать событие сразу всем клиентам.
  2. Как определить, какой клиент заинтересован в получении событий, а какой нет?

Начнем со второго вопроса. Для этих целей интерфейс IConnectionPoint предусматривает специальные методы подписки на события Advise и Unadvise. С помощью первого из них клиент сообщает серверу, что он желает получать уведомления относительно какого-либо события, а вторая функция, наоборот, сообщает серверу, что данный клиент больше в таких сообщениях не нуждается. Соответственно, сервер должен иметь некий список, в который он заносит <адреса> клиентов, подписанных на те или иные сообщения. И из которого он потом будет удалять отписавшихся клиентов. Таким образом, когда серверу нужно будет послать некое событие клиентам, то он в цикле пробежится по этому списку и отправит сообщения всем заинтересованным клиентам. Это и есть ответ на первый вопрос. Теперь приведем процедуру, которая реализует все выше сказанное. Вставьте её код в файл MyInterface.cpp а также не забудьте объявить её в MyInterfcae.h.

void CMyInterface::FireEvent()
{
   const CPtrArray* pConnections=m_xObjCP.GetConnections();

   int nConnections=pConnections->GetSize();

   for (int i=0;i<nConnections;i++)
   {
      IDispatch* pClient=(IDispatch*)(pConnections->GetAt(i));
      if (pClient)
      {

         VARIANT varResult;

         DISPPARAMS disp={0,0,0,0};

         HRESULT hr = pClient->Invoke(0x1,
                        IID_NULL,
                        LOCALE_USER_DEFAULT,
                        DISPATCH_METHOD,
                        &disp,
                        &varResult,
                        NULL,
                        NULL);

      }
   }
}

Итак, что же мы имеем? Скажу сразу, что MFC сильно облегчила нам жизнь. Здесь const CPtrArray* pConnections=m_xObjCP.GetConnections(); мы объявляем указатель на CPtrArray и присваиваем ему значение полученное из m_xObjCP.GetConnections(). Вот и все. Теперь мы имеем указатель на список всех наших потенциальных клиентов. Объект m_xObjCP поддерживается средствами CCmdTarget и самостоятельно, <без нашего участия> заносит в список подписавшихся и удаляет из него отписавшихся клиентов. Дальше все просто: мы перебираем в цикле всех наших клиентов и с помощью метода Invoke <посылаем> им события.

Еще раз напомню: не забудьте добавить описание этой функции в наш класс. А теперь измените тело функции FireMyEvent, как показано ниже.

void CMyInterface::FireMyEvent()
{
   FireEvent();
}

Таким образом, мы заставим <входящий> метод сгенерировать событие. Сохраните все сделанные вами изменения и постройте проект. Ошибок быть не должно. После чего найдите полученную DLL и с помощью утилиты regsvr32.exe (или любого другого инструмента, способного вызвать функцию DllRegisterServer) зарегистрируйте её в вашей операционной системе. На этом с сервером все!

Часть 3. Реализация клиента.

Программа-пример Client

Теперь напишем клиента. Выполните пункты меню File->New и заполните опции согласно рисунку 8.

Рисунок 8

Обратите внимание на папки, куда я помещаю проекты клиента и сервера. Они оба должны находиться в одном каталоге. Жмите Ок, и в появившемся мастере выберите опцию создания диалогового приложения, см. рисунок 9, после чего жмите Finish.

Рисунок 9

Отредактируйте диалоговую форму. Чтобы она приняла вид, как на рисунке 10.

Рисунок 10

Сразу создайте обработчик для кнопки "Fire event", он нам пригодится, а также для кнопки ОК. Теперь необходимо в функции InitInstance класса CPointClientApp добавить инициализацию СОМ. В нашем случае вполне сойдет инициализация для STA, поэтому воспользуемся функцией CoInitialize(NULL):

BOOL CPointClientApp::InitInstance()
{
   AfxEnableControlContainer();
   CoInitialize(NULL);

Сразу же добавьте в этот класс с помощью ClassWizard функцию ExitInstance, а в её обработчике поставьте деинициализацию СОМ:

int CPointClientApp::ExitInstance()
{
   CoUninitialize();return CWinApp::ExitInstance();
}

Все, с этим классом закончили. Сохраните все изменения. Следующим шагом мы должны хорошенько поработать с классом CPointClientDlg. Для этого откройте файл PointClientDlg.h, в котором находится определение этого класса. Сейчас мы снабдим компилятор информацией об интерфейсах нашего СОМ-сервера. Для этого добавьте в PointClientDlg.h до определения самого класса следующую строку (после чего постройте проект):

#import "..\Server\Debug\PointServer.tlb" no_namespace named_guids

Для того, чтобы компилятор смог найти TLB-файл нашего сервера по указанному пути, папки с проектами клиента и сервера должны находиться в общем каталоге. Если вы правильно указывали пути для проектов при их создании, как я просил, то все будет нормально.

Теперь давайте добавим в класс CPointClientDlg (файл PointClientDlg.h) две приватные переменные:

private:
   IMyInterfacePtr   m_MyInterface;
   DWORD         m_dwCookie;

Первая из них есть указатель на интерфейс нашего СОМ-сервера, который мы создавали несколько раньше. Тип IMyInterfacePtr любезно предоставила нам директива #import, после того как мы подключили файл PointServer.tlb. С помощью данного импорта мы приобрели много полезной информации о нашем сервере. Она находится в файлах PointServer.tlh и PointServer.tli, в том числе и определение IMyInterfacePtr. Изучите эти файлы на досуге и, возможно, вы откроете для себя что-то новое.

Вторая переменная (m_dwCookie) есть уникальный идентификатор, который вернет нам функция AfxConnectionAdvise. Помните, я уже рассказывал о механизме подписки клиентов на сообщения от сервера. Тогда мы говорили о функциях Advise и Unadvise интерфейса IConnectionPoint. Здесь же мы будем использовать аналоги этих функций - AfxConnectionAdvise и AfxConnectionUnadvise, предоставляемые библиотекой MFC. Так вот, m_dwCookie - это идентификатор нашей подписки, который вернет нам сервер в случае успешной регистрации нашего соединения. Зачем он нам нужен? Ну, хотя бы для того, чтобы отдать его серверу, когда мы пожелаем отписаться от принятия сообщений, ведь должен же сервер знать, кого он будет удалять из своего списка. Другими словами, m_dwCookie - это аналог того номерка, что дают вам злые тети в раздевалках, если вы сдаете одежду, при походах в театр, библиотеку и т.д. Сохраните сделанные нами изменения и закройте этот файл.

Теперь займемся файлом реализации класса CPointClientDlg. Для этого откройте файл PointClientDlg.cpp, найдите функцию OnInitDialog и строку в ней

// TODO: Add extra initialization here

Вот вместо этого напоминания мы сейчас и напишем наш код. Здесь будет находиться код загрузки СОМ-сервера, а также функция подписки на его события. Итак, вместо комментариев, указанных мастером вставьте следующие строки:

   EnableAutomation();
   UUID   uuid;
   m_MyInterface = NULL;

   uuid = __uuidof(MyInterface);
   m_MyInterface.CreateInstance(uuid);

   m_dwCookie = 0;
   BOOL Ret = AfxConnectionAdvise(
               m_MyInterface,
               DIID_IFireClassEvents,
               this->GetIDispatch(FALSE), // get the IDispatch assocaiated with Mainframe...
               FALSE, // donod addref
               &m_dwCookie ); // cookie to break connection later...

С помощью __uuidof мы получим UUID интерфейса IMyInterface, который затем подставим в функцию CreateInstance. Таким образом, мы вызовем загрузку нашего СОМ-сервера.

После того, как функция CreateInstance будет успешно выполнена, мы подпишемся на сообщения от интерфейса IFireClassEvents с помощью функции AfxConnectionAdvise. В случае корректного завершения которой мы получим наш идентификатор - m_dwCookie. Приведенный код не содержит механизма обработки возможных ошибок, чтобы не загромождать главную идею, которую мы сейчас рассматриваем. В случае необходимости вы можете добавить его сами. Ну вот, к тому моменту, как мы увидим на экране диалог нашего клиента, СОМ-сервер будет уже загружен и готов посылать нам событие, что мы реализовали в его коде.

Сразу же добавим код отписки от событий, который вставим в обработчик нажатия кнопки ОК:

void CPointClientDlg::OnOK()
{
   if(m_MyInterface)
   {
      AfxConnectionUnadvise( m_MyInterface,
                        DIID_IFireClassEvents,
                        this->GetIDispatch(FALSE),
                        FALSE,
                        m_dwCookie );
      m_MyInterface = NULL;
   }

   CDialog::OnOK();
}

Здесь все предельно ясно. Передавая нашу <куку> (m_dmCookie) функции AfxConnectionUnadvise, мы тем самым отписываемся от рассылки событий. После чего делаем m_MyInterface = NULL, чем вызываем выгрузку СОМ-сервера.

Последним штрихом добавим код в обработчик второй нашей кнопки:

void CPointClientDlg::OnFireevent()
{
   m_MyInterface->FireMyEvent();
}

Сохраните все сделанные нами изменения и постройте проект. Если все сделали правильно, то должны были получить 2 сообщения об ошибке, рисунок 11.

Рисунок 11

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

#include <afxctl.h>

Попробуйте снова. Сейчас все должно быть без ошибок.

Устали? Я тоже. Подождите, осталось совсем немного. Сейчас мы реализуем код функции, что будет вызывать у нас сервер, и на этом закончим. Итак, откройте файл PointClientDlg.h и сразу после декларации карты сообщений вставьте ещё несколько определений:

DECLARE_DISPATCH_MAP()
DECLARE_INTERFACE_MAP()

BOOL   OnMyEvent();

Таким способом вы объявите две карты: DISPATCH MAP и INTERFACE MAP, которые нам необходимы. А также объявите обработчик OnMyEvent события MyEvent. Сохраните, сделанные изменения и закройте файл.

Теперь откройте файл реализации класса CPointClientDlg, PointClientDlg.cpp, и сразу после окончания реализации карты сообщений вставьте следующий код:

BEGIN_DISPATCH_MAP(CPointClientDlg, CDialog)
   DISP_FUNCTION_ID(CPointClientDlg, "MyEvent",1, OnMyEvent, VT_BOOL, VTS_NONE)
END_DISPATCH_MAP( )

BEGIN_INTERFACE_MAP(CPointClientDlg, CDialog)
   INTERFACE_PART(CPointClientDlg, DIID_IFireClassEvents, Dispatch)
END_INTERFACE_MAP()

BOOL CPointClientDlg::OnMyEvent()
{
   AfxMessageBox("Event!!!!!!!!");
   return TRUE;
}

Что же это означает?

Во-первых, между макросами BEGIN_DISPATCH_MAP и END_DISPATCH_MAP , с помощью DISP_FUNCTION_ID по номеру метода (1 - см. ODL-файл сервера) мы указываем имя события (MyEvent), его обработчик (OnMyEvent), тип возвращаемого значения (VT_BOOL), а также тип аргументов (VTS_NONE - в данном случае их нет).

Далее идет реализация интерфейсной карты и реализация функции обработчика события OnMyEvent.

На этом, пожалуй, все. Сохраните файл, постройте проект и запустите на выполнение нашего клиента. Если вы все делали правильно, то по нажатию на кнопку "Fire Event", должны получить результат как на рисунке 12.

Рисунок 12

На этом я закончу. Надеюсь, что этот материал кому-то окажет помощь в трудную минуту.

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

Как узнать имя exe-файла выполняемой программы?

Автор: Артур Вартанов

Функция GetModuleFileName возвращает полный путь и имя исполняемого файла. Пример ее использования смотри ниже.

TCHAR FileName[MAX_PATH + 1]; // буфер для имени файла
GetModuleFileName(NULL, FileName, MAX_PATH + 1);

Первый параметр функции GetModuleFileName - дескриптор модуля, для которого требуется получить имя. Если в качестве первого параметра указан hInstance программы или NULL, возвращается имя выполняемой программы. Если же указать дескриптор загруженного модля (DLL), который возвращается функциями LoadLibrary, LoadLibraryEx или GetModuleHandle, возвращается имя этой DLL. Кроме функции GetModuleFileName, существует функция GetModuleFileNameEx, позволяющая получить имя модуля, загруженного в адресное пространство другого процесса.

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Это все на сегодня. Пока!

Алекс Jenter jenter@rsdn.ru
Красноярск, 2001. Рассылка является частью проекта RSDN.


No.50

Выпуск No. 50 от 15 июля 2001 г.

РАССЫЛКА ЯВЛЯЕТСЯ ЧАСТЬЮ ПРОЕКТА RSDN , НА САЙТЕ КОТОРОГО ВСЕГДА МОЖНО НАЙТИ ВСЮ НЕОБХОДИМУЮ РАЗРАБОТЧИКУ ИНФОРМАЦИЮ, СТАТЬИ, ФОРУМЫ, РЕСУРСЫ, ПОЛНЫЙ АРХИВ ПРЕДЫДУЩИХ ВЫПУСКОВ РАССЫЛКИ И МНОГОЕ ДРУГОЕ.

Приветствую вас!

/ / / / СТАТЬЯ / / / / / / / / / / / / / / / / / / / / / /

Отчёты Crystal Reports для Visual C++ 6

Автор: Илья Гуня

Недавно я начал писать один небольшой проект на VC с отчетом Crystal Reports 8 и столкнулся со следующей проблемой: я не знал, как написать отчет. После поиска материалов на эту тему в интернете, у меня сложилось впечатление, что перед разработчиками на VC не стоит проблема создания отчетов. На CodeGuru в разделе Databases я не нашел ни одного материала на эту тему. Пришлось копать эту тему самому. К сожалению, у меня оказался только один пример, в котором довольно сложный отчет полностью создаётся в run-time без использования редактора отчетов. Это автоматически означало, что мне нужно будет изучить несколько десятков, а то и сотен килобайт текста, прежде чем я выдам первый отчет. Времени на это у меня не было. Поэтому для создания отчета я воспользовался следующей технологией, которая и описывается ниже.

Для выполнения этого проекта необходимо:

  • Visual C++ 6
  • Crystal Reports 8

Приступим.

Для начала, создадим наш отчет. Запускаем Crystal Report Designer. Создаем blank report. Добавляем ODBC connection, указывающее, на пример, на БД pubs на вашем SQL сервере, или на какую-нибудь таблицу в mdb-файле. Выбираем таблицу pubs.dbo.authors, давим add кнопку, закрываем окно. В появившемся окне дизайнера отчетов перетаскиваем в область Details нужные поля: au_id, au_fname, au_lname. Сохраняем отчёт.

Создаём простой Dialog-based проект со всеми настройками по умолчанию. В меню Projects->Add to project->Components and controls добавляем Crystal Report Viewer Control. В окне Confirm classes давим OK. Закрываем окно Components and controls. Добавляем Crystal Report Viewer Control на диалог. В окне ClassWizard для диалога добавляем обработчик WM_SHOWWINDOW. At the Member variables tab добавляем переменную m_CRView1. В начало файла SampRepDlg.cpp добавляем строки

#import <craxdrt.tlb> no_namespace
#import <msado15.dll> rename ("EOF", "adoEOF")

(подразумевается, что файл craxdrt.tlb находится в одной из стандартных папок для include. Изначально он находится в каталоге C:\Program Files\Seagate Software\Crystal Reports\Developer Files\include\)

так же добавляем следующие строки в начале файла RepSampDlg.cpp

const CLSID CLSID_Application =
{0xb4741fd0,0x45a6,0x11d1,{0xab,0xec,0x00,0xa0,0xc9,0x27,0x4b,0x91}};
const IID IID_IApplication =
{0x0bac5cf2,0x44c9,0x11d1,{0xab,0xec,0x00,0xa0,0xc9,0x27,0x4b,0x91}};

const CLSID CLSID_ReportObjects =
{0xb4741e60,0x45a6,0x11d1,{0xab,0xec,0x00,0xa0,0xc9,0x27,0x4b,0x91}};
const IID IID_IReportObjects =
{0x0bac59b2,0x44c9,0x11d1,{0xab,0xec,0x00,0xa0,0xc9,0x27,0x4b,0x91}};

Переходим к обработчику CRepSampDlg::OnShowWindow. Я обычно создаю стандартное окружение для работы с COM-объектами:

try{
}
catch(const _com_error& e)
{
    _bstr_t bstrSource(e.Source());
    _bstr_t bstrDescription(e.Description());
    CString strError;
    strError.Format("_com_error catched at
CRepSampDlg::OnShowWindow\n"
    "Source : %s\nDescription : %s",
    (LPCSTR)bstrSource,(LPCSTR)bstrDescription);
    AfxMessageBox(strError);
}

В try-блоке присоединяем наш файл отчета:

HRESULT hr=S_OK;
IApplicationPtr pApp;
IReportPtr pRep;
hr=CoCreateInstance(CLSID_Application, NULL, CLSCTX_INPROC_SERVER , IID_IApplication,
     (void **) &pApp);
if(FAILED(hr)) _com_issue_error(hr);
pRep = pApp->OpenReport(_bstr_t("d:\\projects\\RepSamp\\Report1.rpt"));
m_CRView1.SetReportSource(pRep);
m_CRView1.ViewReport();

Собираем проект, и запускаем. Появится отчет, который в качестве источника данных использует свои настройки по умолчанию. Теперь давайте подставим ему в качестве источника данных необходимый нам Recordset. Я предпочитаю ADO. Следующий код я добавил сразу после строки "HRESULT hr=S_OK;" :

ADODB::_ConnectionPtr pConn;
pConn.CreateInstance(__uuidof(ADODB::Connection));
if(FAILED(hr)) _com_issue_error(hr);
CString sConnStr("Provider=SQLOLEDB.1;"
"Integrated Security=SSPI;Persist Security Info= False;"
"Initial Catalog= pubs;Data Source= DATACENTER");
hr= pConn->Open(_bstr_t(sConnStr),_bstr_t(L""), _bstr_t(L""),
ADODB::adConnectUnspecified);
if(FAILED(hr)) _com_issue_error(hr);
ADODB::_RecordsetPtr pRs;
pRs.CreateInstance(__uuidof(ADODB::Recordset));
CString sSQL("SELECT * FROM authors");
pRs->Open(_bstr_t(sSQL), pConn.GetInterfacePtr(),  ADODB::adOpenDynamic,
     ADODB::adLockOptimistic,ADODB::adCmdText);
if(FAILED(hr)) _com_issue_error(hr);

теперь запихиваем наш recordset в отчет:

IApplicationPtr pApp;
IReportPtr pRep;
hr=CoCreateInstance(CLSID_Application, NULL, CLSCTX_INPROC_SERVER , IID_IApplication,
(void **) &pApp);
if(FAILED(hr)) _com_issue_error(hr);
pRep=pApp->OpenReport(_bstr_t("d:\\proj\\SampRep\\Report1.rpt"));
m_CRView1.SetReportSource(pRep);

IDatabasePtr pDatabase = 0;
IDatabaseTablesPtr pTables = 0;
IDatabaseTablePtr pTable = 0;

pRep->get_Database((IDatabase**) &pDatabase);
pDatabase->get_Tables((IDatabaseTables**) &pTables);

VARIANT var, var2;
VariantInit(&var);
VariantInit(&var2);
var.vt = VT_DISPATCH;
var.pdispVal = (IDispatch*)pConn;
var2.vt = VT_DISPATCH;
var2.pdispVal = (IDispatch*)pRs->GetActiveCommand();

hr = pDatabase->AddADOCommand(var, var2);
ASSERT(SUCCEEDED(hr));

собираем проект. Всё готово.

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

Как сделать нестандартную кнопку на основе битмапа (без MFC, только WinAPI)?

Автор: Игорь Вартанов

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

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

Windows имеет встроенные механизмы и API, поддерживающие создание кнопок (а также и других контролов), имеющих нестандартный внешний вид. Способ отрисовки внешнего вида контрола зависит от его стиля. В данном случае, стиль, нужный нам - это BS_OWNERDRAW. Из его названия видно, что отрисовку вида кнопки выполняет код пользователя, помещенный в оконную (диалоговую) функцию окна-владельца контрола.

Рассмотрим основные этапы отрисовки контрола, имеющего стиль xx_OWNERDRAW.

  1. Родительскому окну контрола приходит сообщение WM_MEASUREITEM, в котором передается указатель на структуру MEASUREITEMSTRUCT через параметр lParam. Обработчик сообщения должен установить значения полей itemWidth и itemHeight структуры так, чтобы они содержали ширину и высоту контрола соответственно. Если мы обработали сообщение, обработчик должен вернуть значение TRUE из оконной процедуры. Это сообщение приходит владельцу один раз при создании контрола.
  2. Каждый раз при необходимости перерисовать контрол его владельцу приходит сообщение WM_DRAWITEM. Параметр lParam сообщения содержит указатель на структуру DRAWITEMSTRUCT, подготовленную системой. В задачу данного сообщения входит предоставление контекста, в котором будет происходить отрисовка контрола. Хэндл контекста сопровождает дополнительная информация о внутреннем состоянии контрола, необходимая (возможно) для изменения его внешнего вида, а также информация о виде действия, производимого в настоящий момент с контролом. Далее мы увидим, каким образом эта информация может быть использована для изменения внешнего вида кнопки. И, опять-таки, если мы обрабатываем данное сообщение, обработчик обязан вернуть из оконной процедуры значение TRUE.

Поскольку мы реализуем, хотя и самостоятельно отрисовываемую, но все же кнопку, то было бы неплохо, если бы она имела поведение обычной кнопки - края кнопки в нормальном состоянии должны имитировать выпуклый контрол, при нажатом состоянии - вдавленный, при установленном фокусе кнопка должна иметь на себе прямоугольник, выполненный пунктирной линией, и в неактивном состоянии кнопка должна резко отличаться по цвету (либо фона, либо надписи, либо и того, и другого).

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

Что касается кода, реализующего необходимую логику работы, то его реализация может быть следующей:

BOOL CALLBACK DlgProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
    static HBITMAP hBm[BM_COUNT] = {NULL, NULL, NULL, NULL};
    . . .
    case WM_DRAWITEM:
        return DrawFreeStyleBtn( (LPDRAWITEMSTRUCT) lParam, hBm );
    . . .
}

BOOL DrawFreeStyleBtn( LPDRAWITEMSTRUCT pis, HBITMAP* phBm )
{
    if( IDC_BMPBTN == pis->CtlID )
    {
        HBITMAP hOld = NULL;
        HBITMAP hbm  = phBm[BM_UP];

        switch(pis->itemAction)
        {
        case ODA_DRAWENTIRE:
        case ODA_SELECT:
            if( pis->itemState & ODS_DISABLED )
                hbm = phBm[BM_DISABLE];
            else
                if( pis->itemState & ODS_SELECTED )
                    hbm = phBm[BM_DOWN];
            break;
        case ODA_FOCUS:
            if( pis->hwndItem == GetFocus() )
                hbm = phBm[BM_FOCUS];
            break;
        }
        HDC hCompDC = CreateCompatibleDC( pis->hDC );
        hOld = (HBITMAP) SelectObject( hCompDC, hbm );
        BitBlt(
            pis->hDC,
            pis->rcItem.left,
            pis->rcItem.top,
            pis->rcItem.right - pis->rcItem.left,
            pis->rcItem.bottom - pis->rcItem.top,
            hCompDC, 0, 0, SRCCOPY );
        SelectObject( pis->hDC, hOld );
        DeleteDC( hCompDC );
        return TRUE;
    }
    return FALSE;
}

Как видим, ничего сложного. Код распадается на две части: в первой на основе сведений о выполняемых действиях (itemAction) и текущем состоянии кнопки (itemState) производится выбор необходимого битмапа, во второй части происходит вывод выбранного битмапа в контекст кнопки. Код обрамляется проверкой на необходимый идентификатор контрола, поскольку в рабочей программе подобных контролов может быть несколько.

Внимательный читатель готов задать вопрос о том, что в самом начале упоминались не только механизмы (реализованные, как мы выяснили, через сообщения WM_MEASUREITEM и WM_DRAWITEM), но и API?

Действительно, имеется несколько функций, облегчающих придание стандартного вида OWNERDRAW-контролам. Разработчик готовит только основной битмап для кнопки, а для отрисовки границ и состояний кнопки (неактивное и в фокусе) пользуется функциями WinAPI - DrawEdge() (границы контрола - "выпуклый/вдавленный"), DrawState() (состояние "активный/неактивный") и DrawFocusRect() (состояние "в фокусе"). В таком случае вышеприведенный код примет вид:

BOOL CALLBACK DlgProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
    static HBITMAP hBm = NULL;
    . . .
    case WM_DRAWITEM:
        return DrawClassicStyleBtn( (LPDRAWITEMSTRUCT) lParam, hBm );
    . . .
}

void DrawClassicStyleBtn(LPDRAWITEMSTRUCT pis, HBITMAP hBm, int deflate = 4)
{
    UINT uState = DSS_NORMAL;
    UINT uEdge  = EDGE_RAISED;
    int  x = 0, y = 0;
    BOOL bFocus = FALSE;
    RECT rFocus;

    if(IDC_BMPBTN == pis->CtlID)
    {
        switch(pis->itemAction)
        {
        case ODA_DRAWENTIRE:
        case ODA_SELECT:
            if(pis->itemState & ODS_DISABLED)
            {
                uState = DSS_DISABLED;
            }
            else
                if(pis->itemState & ODS_SELECTED)
                {
                    x += 1;  // сдвиг всего рисунка вправо-вниз подчеркивает
                    y += 1;  // визуальный эффект нажатия кнопки
                    uEdge  = EDGE_SUNKEN;
                }
            break;
        case ODA_FOCUS:
            if(pis->hwndItem == GetFocus())
            {
                memcpy(&rFocus, &pis->rcItem, sizeof(RECT));
                rFocus.left   += deflate;
                rFocus.top    += deflate;
                rFocus.right  -= deflate;
                rFocus.bottom -= deflate;
                bFocus = TRUE;
            }
            break;
        }
        DrawState(
            pis->hDC, NULL, NULL, (LONG)hBm, 0,
            x, y, 0, 0, DST_BITMAP | uState);
        DrawEdge(pis->hDC, &pis->rcItem, uEdge, BF_RECT);
        if(bFocus)
            DrawFocusRect(pis->hDC, &rFocus);
    }
}

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

Следует заметить, что при необходимости можно (а иногда и нужно) пользоваться комбинацией приведенных методик: предположим, использовать для отрисовки чертыре битмапа, но границу рисовать функцией DrawEdge().

При подготовке данного материала мною использован код, опубликованный в одном из сообщений эхоконференции SU.WIN32.PROG (FidoNet). Автор кода - Dmitry Timoshkov <dmitry@sloboda.ru> - вполне может и не узнать его, поскольку код был мною довольно сильно переработан и дополнен :-))).

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

На сегодня все. До встречи!

Алекс Jenter jenter@rsdn.ru
Красноярск, 2001. Рассылка является частью проекта RSDN.


Выпуск No. 51 от 21 октября 2001 г.

РАССЫЛКА ЯВЛЯЕТСЯ ЧАСТЬЮ ПРОЕКТА RSDN , НА САЙТЕ КОТОРОГО ВСЕГДА МОЖНО НАЙТИ ВСЮ НЕОБХОДИМУЮ РАЗРАБОТЧИКУ ИНФОРМАЦИЮ, СТАТЬИ, ФОРУМЫ, РЕСУРСЫ, ПОЛНЫЙ АРХИВ ПРЕДЫДУЩИХ ВЫПУСКОВ РАССЫЛКИ И МНОГОЕ ДРУГОЕ.

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

Наконец-то вы дождались нового сезона выхода рассылки! Вы наверное уже наверняка заметили, что отпуск немного (честно говоря, намного) затянулся. Это связано с моим переездом - получилось так, что мне повезло выиграть стипендию на прохождение магистерской программы в немецком университете. В такой ситуации без всяческих проблем не обойтись, и надо сказать адаптация в Германии заняла гораздо больше времени, чем я рассчитывал. Да и интернет тут у меня появился совсем недавно. Так что прошу прощения за столь долгую отлучку. Теперь все в порядке, и рассылка опять будет набирать обороты.

Видя, что рассылка так долго не выходит, меня спрашивали, не нужна ли помощь. Отвечаю - помощь всегда нужна! Сейчас помощь больше всего нужна сайту RSDN - в том смысле, что нам не хватает талантливых людей, готовых вести разделы на сайте, верстать статьи и пр. и пр. Если вы чувствуете себя в силах помочь - милости просим, пишите на team@rsdn.ru!

И еще один организационный вопрос. Рассылка, как и раньше, будет выходить один раз в неделю и предположительно по воскресеньям, хотя день может и меняться.

Подключившись наконец к интернету и забрав накопившиеся письма, я узнал, что в выложенном мной перед уходом в отпуск chm-файле с архивом рассылки есть некоторые недоработки - а именно, с поиском. Это в принципе неудивильно, так как создавался этот файл достаточно спешно ;-) Я в ближайшее время постараюсь разобраться с этим.

Ну, это пока все, что я хотел сказать. Теперь - вперед, к вершинам мастерства!

/ / / / СТАТЬЯ / / / / / / / / / / / / / / / / / / / / / /

Использование ListView в режиме виртуального списка

Автор: Тимофей Чадов

Демонстрационная программа - 161 KB
Исходные тексты - 39 KB

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

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

Конечно, как и везде, нужно знать меру. Не стоит сломя голову бежать переписывать свой код, если требуется вывести диалоговое окно для выбора десятков элементов: cойдет и обычный подход. Однако, если логика вашего приложения основана на применении представления на основе ListView с широкими возможностями по добавлению|удалению|редактированию, да к тому же большого объема записей, - стоит задуматься о виртуальном режиме.

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

Виртуальность это просто

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

  • Создать виртуальный список
  • Вовремя заботиться о количестве элементов
  • По запросу списка заполнять элементы нужной информацией

В некоторых случаях возможно понадобятся и более сложные вещи:

  • Кеширование
  • Сортировка
  • Быстрый поиск элементов

Итак, обо всем по порядку.

Переход в режим виртуальности

Чтобы включить режим "виртуальности", необходимо установить стиль LVS_OWNERDATA. Текущая версия библиотеки элементов управления не позволяет переводить список из обычного режима в виртуальный "на лету", поэтому установку данного стиля необходимо делать при создании элемента. Если вы использует редактор диалога достаточно отметить переключатель Owner Data на вкладке More Style в окне свойств List Control. В случае применения класса СListView следует перекрыть PreCreateWindow.

Вся прелесть виртуального режима в том, что список хранит не сами элементы, а только их общее количество, диапазон отображаемых в данный момент, и тому подобную "мелочь". Значения самих элементов (строковые метки, рисунки и т.п) запрашиваются у приложения непосредственно перед их отрисовкой на экране. Такой прием позволяет значительно сэкономить память и существенно повысить производительность, особенно для больших объемов данных.

ПРИМЕЧАНИЕ
В MSDN сказано, что после установки данного стиля, число элементов, которые сможет хранить список, будет ограничено максимальным значением DWORD (для обычных списков только int). Однако, все функции (в том числе и API) для работы со списком принимают int. Кроме этого, мне не удалось использовать более 100.000.000 элементов. Более того, в примере MSJ за ноябрь 1996 г. от Strohm Armstrong встречается именно эта магическая цифра. Отговорка, стандартна: "Сложно представить, что возникнет необходимость использовать больше". Нет вопросов, если бы использовалась хотя бы степень двойки, а так, IMHO, ограничение такой странной (круглой) цифрой выглядит коварным замыслом.

Количество элементов

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

void CListCtrl::SetItemCount( int iCount )
void CListCtrl::SetItemCountEx( int iCount, DWORD dwFlags = LVSICF_NOINVALIDATEALL );
iCount | новое количество элементов
dwDlags | Комбинация флагов, определяющая реакцию списка на изменение количества элементов. LVSICF_NOINVALIDATEALL Список не будет перерисован, пока добавленные элементы не окажутся с поле видимости. LVSICF_NOSCROLL Позиция скроллинга не изменится.

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

Содержание элементов

Итак, виртуальный список хранит очень мало информации. За заполнение элементов перед отрисовкой отвечает приложение. Для этого список посылает уведомление LVN_GETDISPINFO. Обработчик несложно добавить, воспользовавшись ClassWizzard.

В обработчике уведомления LVN_GETDISPINFO необходимо проверить, какая информация требуется, и заполнить соответствующие поля.

В следующем примере показан один из способов реализации.

void CMyListView::OnGetdispinfo(NMHDR* pNMHDR, LRESULT* pResult)
{
    LV_DISPINFO* pDispInfo = (LV_DISPINFO*)pNMHDR;
    LV_ITEM* pItem= &(pDispInfo)->item;

    CMyDocument* pDoc = GetDocument();
    int nIndex= pItem->iItem;

    if (pItem->mask & LVIF_TEXT) //требуется текст?
        strcpy(pItem->pszText, pDoc->GetItemText(pItem->iSubItem, nIndex));

    if pItem->mask & LVIF_IMAGE) //требуется картинка
        pItem->iImage=  pDoc->GetItemImage(nIndex);
}

Здесь GetItemText и GetItemImage функции класса документа, возвращающие текст меток и номер изображения требуемого элемента соответственно.

По умолчанию виртуальный список не хранит информацию поля state, за исключением двух флагов LVIS_SELECTED и LVIS_FOCUSED. Это приводит к тому, что использование иконок состояния (state image) невозможно. Однако эту ситуацию легко исправить. Необходимо использовать сообщение LVM_SETCALLBACKMASK, позволяющее задать маску для хранимой списком информации об элементах.

// Разрешаем использовать иконки состояния
SendMessage( LVM_SETCALLBACKMASK , LVIS_STATEIMAGEMASK , 0)
Кажущиеся трудности

Итак, список создан и замечательно работает. Возможно, в некоторых случаях, Вам захочется реализовать и более сложные вещи.

Управление кешированием

Если вы самостоятельно решили оперировать большими объемами информации - без кеширования не обойтись. Виртуальный список помогает оперировать процессом кеширования, посылая приложению уведомления LVN_ODCACHEHINT, в которых передает информацию о диапазоне элементов, которые необходимо отобразить на экране. Эту информацию можно использовать для динамического выделения памяти под требуемое число элементов и заполнения их корректными значениями.

void CMyListView::OnOdcachehint(NMHDR* pNMHDR, LRESULT* pResult)
{
    NMLVCACHEHINT* pCacheHint = (NMLVCACHEHINT*)pNMHDR;
    // Подготовить кеш
    PrepareCach( pCacheHint->iFrom, pCacheHint->iTo);
    *pResult = 0;
}

Реализация функции, подобной PrepareCach, зависит от того, каким способом вы храните и обрабатываете данные, и может быть различной в зависимости от решаемой задачи. Данное уведомление всего лишь своевременная подсказка.

Нахождение специфических элементов

Когда списку необходимо найти специфический элемент, он посылает уведомление LVN_ODFINDITEM. Это может случиться, если требуется реализовать нажатие быстрой клавиши (поиск по имени), или элемент получил сообщение LVM_FINDITEM. Информация для поиска передается в двух структурах NMLVFINDITEM и LVFINDINFO. В них содержится: номер элемента, с которого следует начать поиск; элемент искомой строки; направление поиска и т.п.

void CMyListView::OnOdfinditem(NMHDR* pNMHDR, LRESULT* pResult)
{
    NMLVFINDITEM* pFindInfo = (NMLVFINDITEM*)pNMHDR;
    LVFINDINFO FindItem = pFindInfo->lvfi;

    if (FindItem.flags & LVFI_STRING)
    {
        //ишем FindItem.psz начиная pFindInfo->iStart
        *pResult = GetDocument()->FindItem( FindItem.psz, pFindInfo->iStart)
        return;
    }
    *pResult = -1;
}

Обработчик уведомления должен вернуть номер искомого элемента или -1 в случае неудачи.

Сортировка

Трудности? Это еще что такое? Однако бесплатный сыр сами знаете где. Дело в том, что, так как сами элементы в списке не хранятся, придется самим заботится о сортировке. Не удастся воспользоваться функцией CListCtrl::SortItems, бесполезно писать CompareItems и т.п. Все, что у вас есть - это порядковый номер элемента.

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

Альтернатива в заключение

Виртуальный режим - не единственный способ заставить список запрашивать информацию об элементах. Можно при добавлении элемента задать значение pszText структуры LVITEM равным LPSTR_TEXTCALLBACK. В этом случае, также будут приходить уведомления LVN_GETDISPINFO. Однако при этом, придется самостоятельно заботиться о добавлении|удалении элементов, вместо одного вызова SetItemCount для виртуального режима. Кроме того, не будет заметного выигрыша в экономии памяти и скорости. Более подробно данный способ описан в статье Chris Maunder. Using text callbacks in ListView Controls

Напоследок, небольшое резюме. Как вы уже поняли, элемент управления ListView достаточно гибок в использовании, и для написания качественного кода, важно не ошибиться в выборе необходимого режима работы. У каждого, как всегда, свои плюсы и минусы. Выбор (и ответственность) за Вами. Я всего лишь хотел помочь разобраться с этим.

P.S. Специальное спасибо Willi за подсказку об иконках состояния.

/ / / ЭКЗАМЕН / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

В этой новой рубрике будут публиковаться вопросы из самых различных экзаменов и тестов по Visual C++, WinAPI, MFC и др., а также конечно ответы на них ;-). Надеюсь вы найдете эту рубрику полезной.

  What synchronization object can only be used to synchronize threads for a single process?

   1. CriticalSection
   2. Semaphore
   3. Mutex
   4. Timer
   5. Event

Верный ответ - 1. Critical Section. Критические секции не могут быть использованы для синхронизации потоков, принадлежащих разным процессам.

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Это все на сегодня. До встречи!

Алекс Jenter jenter@rsdn.ru
Duisburg, 2001. Рассылка является частью проекта RSDN.


ПРОГРАММИРОВАНИЕ НА V I S U A L C + +
РАССЫЛКА САЙТА
RSDN.RU
Выпуск No. 52 от 28 октября 2001 г.

РАССЫЛКА ЯВЛЯЕТСЯ ЧАСТЬЮ ПРОЕКТА RSDN , НА САЙТЕ КОТОРОГО ВСЕГДА МОЖНО НАЙТИ ВСЮ НЕОБХОДИМУЮ РАЗРАБОТЧИКУ ИНФОРМАЦИЮ, СТАТЬИ, ФОРУМЫ, РЕСУРСЫ, ПОЛНЫЙ АРХИВ ПРЕДЫДУЩИХ ВЫПУСКОВ РАССЫЛКИ И МНОГОЕ ДРУГОЕ.

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

Как вы заметили, внешний вид выпусков немного изменился - чтобы отметить новый сезон. Я надеюсь, в лучшую сторону ;-) Но судить конечно вам. Присылайте свои соображения.

Сегодня в рассылке впервые будет затронута тема DirectX (давно пора, надо сказать). И, конечно, вас ждут еще рубрики "Вопрос-Ответ" и "Экзамен".

CТАТЬЯ

Введение в Direct3D8
Автор: Александров Алексей

Демонстрационное приложение (только .exe) (72 kb)
Демонстрационное приложение (исходный код) (44 kb)

Рисунок: демонстрационное приложение

Компьютерная графика всегда была одним из самых интересных проявлений развития информационных технологий. Давным-давно, во времена текстовых терминалов никто даже и представить себе не мог, что пройдет совсем немного времени и образы фантастических монстров будут создаваться не с помощью папье-маше и пластилина, а прямо на экране компьютера. Это сейчас все привыкли к графическому интерфейсу, и изображением на экране уже никого не удивишь. На самом деле надо поставить памятник человеку, который впервые решил нарисовать картинку из текстовых символов - такого рода искусство было очень распространено в свое время, и его отголоски до сих пор встречаются в различного рода конференциях. История развития компьютерной графики интересна сама по себе и заслуживает отдельной книги, но данный документ имеет чисто технический характер, поэтому мы не будем останавливаться на этапах становления искусства рисования на экране монитора, а обратимся сразу к нашим дням. Стандартом де-факто на компьютерах под управлением операционной системы Windows стали две 3D библиотеки: OpenGL и Direct3D (часть библиотеки DirectX). OpenGL, разработанный фирмой Silicon Graphics, уже стал классикой и характеризуется своей устойчивостью и стабильностью интерфейсов. Напротив, Direct3D, детище Microsoft, постоянно изменяется, совершенствуется и двигается вперед. Последняя версия этого продукта имеет номер 8, и это не конец. В этой статье я бы хотел познакомить читателя с некоторыми аспектами использования этого нового продукта, указать на его отличия от предыдущей версии и продемонстрировать его использование для решения одной из весьма распространенных задач - построения графика функции двух переменных. Исходный код примера вы может получить с этого сайта и свободно использовать в своих приложениях.

DirectX 8.0a SDK можно найти здесь.

Англоязычную версию DirectX 8.0a Runtime for Windows 95, Windows 98, Windows 98 SE, Windows ME можно найти здесь.

Англоязычную версию DirectX 8.0a Runtime for Windows 2000 можно найти здесь.

Локализованные версии DirectX 8.0a Runtime расположены тут.

Перед тем как начать, хотелось бы еще сказать несколько слов по поводу терминологии, используемой в статье. Приведенная информация базируется в основном на документации от Microsoft, которая доступна пока (и похоже, что так будет всегда) исключительно на английском языке. Не то, что бы никто у нас не знает английского языка, но уж человеческая психика так устроена, что одни и те же термины все авторы переводят на русский по своему. Поэтому мной было принято решение: термины, по которым читатель скорее всего захочет узнать больше из официальной документации, оставлять на их родном языке. Так что не удивляйтесь, увидев термин "flip chain" вместо "последовательность отображения" или "цепочка переворота"... В некоторых случаях представлен перевод термина с указанием в скобках оригинального слова или выражения. Например: "матрица проектирования (projection matrix)".

Немного о демонстрационном приложении.

Любой автор статьи рано или поздно сталкивается с нелегким вопросом: какую среду разработки использовать для иллюстрирования излагаемого материала. Даже после выбора Visual C++ в качестве базы остается несколько альтернативных путей: уж больно много расплодилось различных библиотек и frameworks. Можно выделить 4 наиболее заметных (сразу же отмечу их недостатки и достоинства):

  • Чистое API приложение. Небольшое по размеру получаемого исполняемого файла, теоретически легко переносимое. Исходный код, правда, компактностью не отличается...
  • MFC приложение. Наиболее распространенный выбор. Сейчас уже трудно найти компьютер, на котором отсутствует mfc42.dll, хотя, распространяя приложение, вы должны предусмотреть все варианты. К недостаткам можно отнести некоторую угловатость и тяжеловесность исходников.
  • WTL (Windows Template Library) приложение. Замечательная штука, но почему-то еще не все люди слышали о WTL, и, что еще более печально, не у всех она установлена.
  • И, наконец, ATL приложение. Наиболее любимый мною подход, пригодная (вопреки общему мнению) для создания практически любого Windows-приложения. К сожалению, мастер ATL из комплекта Visual C++6 не поддерживает генерацию не-COM приложения.

После долгих размышлений я остановился на последнем варианте. Как говорится, читателю все равно, а мне приятно. Шутка. На самом деле, я действительно считаю, что этот подход позволил мне сделать код максимально понятным, и человек, не измученный нарзаном в виде MFC, разберется там без особых проблем. Если вы ненавидите ATL - не читайте эту статью дальше. Чтобы внести некоторую ясность и определенность, спешу представить вашему вниманию диаграмму классов для демо-приложения. Выполнено в Rational Rose 2000 - нотация Буча.

Рисунок: диаграмма классов

Несколько комментариев по назначению реализованных классов:

  • CMainDlg - Главный класс приложения. Унаследован от CDialogImpl и создается в функции WinMain как немодальный диалог. Содержит в себе один экземпляр класса C3DGraphic, один C3DGraphFrame, 4 немодальных диалога редактирования свойств (CMaterialPropsWindow, CLightPropsWindow, CBackColorWindow and CFunctionTypeWindow) и 3 объекта 3D функций (CSplashFunction, CPlaneFunction and CParabaloidFunction).
  • CPropertyWindow - Базовый класс для всех окон редактирования свойств. Унаследован от CDialogImpl.
  • C3DFunction - Базовый абстрактный класс, определяющий интерфейс получения информации о какой-либо функции 2-х переменных.
  • CPropertyWindowNotify - Абстрактный класс-интерфейс, реализуемый клиентами окон свойств. Через этот интерфейс клиенты уведомляются об изменениях, происходящих со свойствами.
  • CD3D8Application - Весьма простой класс-обертка для управления жизнью и смертью IDirect3D8 объекта.
  • C3DGraphFrame - Окно, в котором будет отображаться результирующая 2D проекция трехмерного изображения. Говоря в терминах MFC, класс вида для трехмерного графика.
  • C3DGraphic - Наиболее значимый и нагруженный класс, выполняющий всю основную работу по построению и обработке 3D картинки. Именно он реализует операции управления светом, свойствами материала, рендеринга и тому подобные. Собственно, ради него все и затевалось...

То ли Микеланджело, то ли еще кого-то из великих однажды спросили, как создать скульптуру. Великий, недолго думая, ответил: "Возьмите большой камень и удалите все лишнее." Окинув взглядом все вышеизложенное, можно сделать простой вывод: демо-приложение является ни чем иным, как обычным ATL EXE COM сервером, из исходного кода которого хитрой рукой автора было удалено все относящееся к COM технологии. Построение такого приложение само по себе является интересной задачей, но мы не скульпторы, так что подробности оставим в стороне. Тем более, что тема моей статьи все-таки DirectX, а не ATL. Вот сейчас - как раз об этом...

А что это за Direct3D8, и где оно живет???

Если вы когда-либо имели дело с предыдущими версиями DirectX, то Вас ждет очень интересное открытие: DirectDraw больше нет вообще!!! Microsoft в очередной раз решил обобщить все что можно, и теперь все относящееся к рисованию в DirectX представлено под одним общим заголовком DirectX Graphics. Все изменилось кардинальным образом по сравнению с DirectX7. Интерфейс IDirect3DDevice8, например, имеет 94 метода. Для сравнения, IDirect3DDevice7 позволял выполнить лишь 48 операций. Back-буферизация теперь поддерживается автоматически, без каких-либо усилий с Вашей стороны - не надо больше вручную создавать эти загадочные flip chains. Инициализация Direct3D стала простой как i++, и многие низкоуровневые детали теперь стали вообще недоступны программисту. Не всегда это здорово - например, теперь вы не можете рисовать что-либо непосредственно на primary surface. Более того, вы и читать то с нее ничего не можете. Точнее можете, но Microsoft не советует. От себя посоветую никогда не делать что-либо нерекомендованное Microsoft, а любопытных отсылаю к документации по методу IDirect3DDevice8::GetFrontBuffer. Впрочем, к вопросам совместимости Microsoft всегда относилась внимательно, и, установив DirectX8, Вы можете свободно работать и со всеми предыдущими версиями этого продукта.

А что же делать тем людям, которые никогда не имели дела с Direct3D вообще??? Специально для них я бы хотел вкратце обрисовать основное назначение этой библиотеки (предполагается, что Вы все же знакомы с теоретическими аспектами построения 3D моделей):

  • Direct3D8 обеспечивает аппаратно-независимый путь доступа к возможностям видеооборудования, установленного на машине. Если запрошенные возможности не поддерживаются видеокартой, библиотека обеспечивает прозрачную эмуляцию. Эмуляция работает намного медленнее, да и не все эмулируется...
  • Поддерживается стандартный конвейер 3D преобразований: матрица окружения (world matrix), матрица моделирования (view matrix) и матрица проектирования (projection matrix).
  • Поддержка растеризации различных геометрических примитивов: точек, отрезков, треугольников. Примитивы более высокого уровня тоже имеются, но только при соотвествующей поддержке оборудованием - эмуляция не обеспечивается.
  • Очень мощная подсистема освещения: свет и материалы.
  • 3D текстурирование. Очень забавная и гибкая вещь, позволяющая делать поистине впечатляющие вещи: можно натянуть любое изображение на любой 3D объект.
  • За деталями упомянутого и всего остального - добро пожаловать в DirectX 8.0a SDK.
С чего начать?

Работа с любой новой средой разработки или библиотекой начинается, как правило, с одного и того же вопроса: "Боже мой! Ну почему оно не компилируется???!!!". Для успешной компиляции Direct3D8 проекта Вам необходимо включить некоторые заголовочные файлы и скомпоновать Вашу программу с соотвествующими lib-файлами. Наиболее важными являются 2 заголовочных и 2 lib-файла:

  • d3d8.h - Файл с определениями основных интерфейсов, констант и тому подобного.
  • d3d8.lib - Файл для компоновки Вашей программы с динамической библиотекой Direct3D8.
  • d3dx8.h - Вспомогательные интерфейсы и определения, несколько облегчающие жизнь среднестатистическому программисту.
  • d3dx8.lib - Библиотека для компоновки программы с d3dx8.dll.

Среди файлов демо-проекта вы увидите 2 файла: D3D8Include.h и D3DX8Include.h. Просто включите их в файл stdafx.h Вашего проекта. Тем самым будут включены все необходимые header-файлы и обеспечена компоновка с соотвествующими lib-файлами.

Ну вот, вроде бы все готово к бою и что же теперь? Первым делом необходимо создать IDirect3D8 объект. Не совсем, правда, грамотно звучит. Правильнее было бы сказать, "необходимо создать некий объект, который выставляет интерфейс IDirect3D8", но, думаю, Вы мне простите подобные вольности. Объект IDirect3D8 обеспечивает создание 3D устройств, получение информации о возможностях устройства, перечисление видеорежимов адаптера и получение подробной информации о них, в общем, кучу всего интересного и нужного. Мой демо-проект для создания этого нужного объекта использует класс-обертку CD3D8Application. Для его использования надо просто унаследовать от него класс приложения и в какой-нибудь функции инициализации (например, в OnInitDialog) вызвать метод CD3D8Application::Direct3DInitOK() для проверки результата создания объекта. Объект IDirect3D8 создается в конструкторе класса CD3D8Application с помощью вызова функции Direct3DCreate8(). Замечательная функция в том плане, что принимает только один параметр, да и тот просто обязан быть D3D_SDK_VERSION. Таким образом, создание требуемого объекта сводится к следующему:

    pDirect3DObject = Direct3DCreate8(D3D_SDK_VERSION);
    if (!pDirect3DObject) {
        // Do something!!! Error occured!!!
    }

После создания объекта IDirect3D8 мы должны создать еще что-то, на чем мы будем рисовать. Это "что-то" называется IDirect3DDevice8 и мы можем его создать с помощью метода IDirect3D8::CreateDevice(). В демо-проекте эта функция вызывается из C3DGraphic::Create() следующим образом:

    D3DDISPLAYMODE theDisplayMode;
    hr = m_p3DApplication->m_pDirect3DObject->GetAdapterDisplayMode(
        D3DADAPTER_DEFAULT, &theDisplayMode);
    if (FAILED(hr)) {
        return hr;
    }

    D3DPRESENT_PARAMETERS thePresentParams;
    ZeroMemory(&thePresentParams, sizeof(thePresentParams));
    thePresentParams.Windowed   = TRUE;
    thePresentParams.SwapEffect = D3DSWAPEFFECT_DISCARD;
    thePresentParams.BackBufferFormat = theDisplayMode.Format;
    thePresentParams.EnableAutoDepthStencil = TRUE;
    thePresentParams.AutoDepthStencilFormat = D3DFMT_D16;

    hr = m_p3DApplication->m_pDirect3DObject->CreateDevice(
        D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL,
        m_hwndRenderTarget,
        D3DCREATE_SOFTWARE_VERTEXPROCESSING,
        &thePresentParams,
        &m_p3DDevice);

    if (FAILED(hr)) {
        return hr;
    }

Только что мы создали 3D устройство, задав при этом кучу параметров. Несколько комментариев и разъяснений, что есть что:

  • 3D устройство было создано для видеоадаптера, используемого по умолчанию. Об этом говорит параметр D3DADAPTER_DEFAULT. Средний житель России имеет от 0 до 1 видеоадаптеров, так что особых проблем с этим параметром возникнуть не должно.
  • Созданное устройство имеет один back-буфер, цветовой формат буфера такой же как у экрана.
  • Устройство создано для использования в оконном режиме. Весь рендеринг будет направлен на окно m_hwndRenderTarget. Эта переменная соотвествует окну нашего вида C3DGraphFrame. Антиподом оконному режиму служит полноэкранный режим. Для работы с ним надо при создании устройства переменную Windowed установить в FALSE, а не в TRUE.
  • Устройство имеет автоматически созданный depth-буфер, который иногда еще называют z-буфером. Формат z-буфера - 16 бит на точку. В наше время трудно (но, правда, можно) найти видеоадаптер, который не поддерживает такой формат z-буфера. Z-буфер используется вычислительным ядром Direct3D для хранения информации о текущей z-координате каждой точки картинной плоскости. В конечном счете это используется для удаления невидимых линий и поверхностей.
  • 3D устройство должно использовать все возможности аппаратуры. Если какое-либо требуемое свойство не поддерживается, Direct3D попытается эмулировать его на программном уровне. Вы можете поменять тип устройства с D3DDEVTYPE_HAL на D3DDEVTYPE_REF. В этом случае все вычисления будут проводиться на программном уровне. Такая эмуляция является очень медленной, и не все на свете можно эмулировать.
  • Обрабатываться вершины (vertexes) должны на программном уровне. Мы сделали этот выбор, указав значение D3DCREATE_SOFTWARE_VERTEXPROCESSING. Можно указать вместо этого D3DCREATE_MIXED_VERTEXPROCESSING или D3DCREATE_HARDWARE_VERTEXPROCESSING, но эти режимы поддерживаются далеко не всеми видеокартами.

Стоит заметить, что реальное приложение (игра, например) должны вначале анализировать возможности установленного оборудования, а затем уже принимать решение о возможности или, наоборот, невозможности продолжать работу. Для упрощения такая проверка в демо-приложении не делается.

От f(x,y)=sin(x+y) до 2D картинки

Жизнь всегда была сложнее, чем хотелось бы программисту. Например, все было бы гораздо проще, если бы существовала некая функция ХочуЧтобыНарисовалсяГрафикФункции(). Но во-первых, по-русски функции называть нельзя, во-вторых, такой функции просто нет. Все, что умеет IDirect3DDevice8, это нарисовать некоторые примитивы, да и для этого надо проделать кучу подготовительной работы. Как минимум, вы должны разместить координаты примитивов в вертекс-буфер ("vertex buffer"). Полное обсуждение вертекс-буферов находится за пределами этой статьи и могу лишь сказать, что это некая абстракция простого блока памяти, предназначенного для хранения координат и свойств точек трехмерного пространства. Работа с буфером осуществляется через интерфейс IDirect3DVertexBuffer8. Вы может заблокировать вертекс-буфер, получив при этом указатель на область памяти. Разыменовав указатель, можно записать что-нибудь в буфер, не забыв потом его разблокировать. В демо-приложении вы можете увидеть как это делается.

Обычно перед рисованием чего-либо мы хотим очистить кадр, чтобы новый кадр не накладывался на предыдущий. Это делается вызовом метода IDirect3DDevice8::Clear(). Вы найдете это в функции C3DGraphic::ReRender():

    hr = m_p3DDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,
        m_dwBackColor, f, 0);
    if (FAILED(hr)) {
        return hr;
    }

Мы здесь очищаем весь back-буфер, заполняя его цветом m_dwBackColor. Кроме этого, очищается z-буфер - он заполняется значением 1.0. 1.0 соответствует максимально дальней от наблюдателя плоскости (горизонт, грубо говоря), 0.0 - максимально ближней. Непосредственно рисование на 3D устройстве начинается с

    m_p3DDevice->BeginScene();

а заканчивается строкой

    m_p3DDevice->EndScene();

Все, что находится между этими вызовами символизирует анимацию очередного кадра. Обратите внимание на то, что мы постоянно употребляем термин "back-буфер". Совершенно верно! Все, что мы сейчас нарисовали, не видно на экране, оно существует пока только в памяти компьютера (не будем уточнять, в какой именно: системной или видео - очень тонкий вопрос). Для того, чтобы эти изменения перенеслись на экран монитора необходимо скопировать изображение из back-буфера на основную поверхность. Это делается вызовом функции IDirect3DDevice8::Present().

Следующий код обеспечивает непосредственно отрисовку изображения трехмерной функции:

    hr = m_p3DDevice->SetStreamSource(0, m_pDataVB, sizeof(GRAPH3DVERTEXSTRUCT));
    if (FAILED(hr)) {
        return hr;
    }

    hr = m_p3DDevice->SetVertexShader(D3DFVF_GRAPH3DVERTEX);
    if (FAILED(hr)) {
        return hr;
    }

    hr = m_p3DDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, m_dwElementsInVB - 2);
    if (FAILED(hr)) {
        return hr;
    }

Переменная m_pDataVB является членом класса C3DGraphic, она содержит указатель на интерфейс IDirect3DVertexBuffer8. В нашем случае содержимое вертекс-буфера представляет из себя массив структур GRAPH3DVERTEXSTRUCT:

typedef struct {
    FLOAT x, y, z;
    FLOAT nx, ny, nz;
} GRAPH3DVERTEXSTRUCT;

Здесь x, y и z - координаты точки, nx, ny, nz - компоненты нормали. Вектор нормали используется подсистемой Direct3D, отвечающей за освещение сцены. Чуть позже мы рассмотрим, как можно рассчитать эти компоненты. Перед вызовом функции DrawPrimitive() мы должны указать vertex shader и используемый поток данных. Выбор и установка потока данных производится с помощью функции SetStreamSorce(). Мы передаем ей номер устанавливаемого потока (0, если оспользуется только один поток), указатель на вертекс-буфер, из которого будут браться данные, и размер каждого элемента в потоке (то есть, sizeof(GRAPH3DVERTEXSTRUCT). Vertex shaders - новая возможность Direct3D. Это эдакая абстракция, символизирующая обработку вершин (вертексов). Грубо говоря, vertex shader обеспечивает перевод точки из трехмерного пространства модели в двумерную картинную плоскость. Вы можете написать на особом языке скрипт, обеспечивающий этот перевод, а можете использовать один из поддерживаемых стандартных механизмов, что мы и сделаем. Наш vertex shader D3DFVF_GRAPH3DVERTEX определен как D3DFVF_XYZ | D3DFVF_NORMAL. Это означает, что каждая точка характеризуется 6-ю числами. Первые 3 из них трактуются как координаты вершины, остальные - как компоненты нормали.

После установки необходимых потока данных и вертекс-шейдера мы можем вызвать метод DrawPrimitive(), который отображает данные из вертекс-буфера на плоскость back-буфера. Способ рендеринга данных выбирается первым параметром этого метода - он может быть одним из значений перечисляемого типа D3DPRIMITIVETYPE: D3DPT_POINTLIST, D3DPT_LINELIST, D3DPT_LINESTRIP, D3DPT_TRIANGLELIST, D3DPT_TRIANGLESTRIP или D3DPT_TRIANGLEFAN. Я решил, что наиболее подходящим способом для построения графика функции является использование D3DPT_TRIANGLESTRIP, поскольку этот способ требует относительно немного памяти для хранения данных о поверхности. Триангуляции области построения графика осуществляется как показано на следующем рисунке.

Рисунок: триангуляции

Все это работает следующим образом: вертекс-буфер содержит точки P1, P2, P3, P4 и так далее. Когда я вызываю функцию DrawPrimitive() с параметром D3DPT_TRIANGLESTRIP, Direct3D начинает отображать треугольники 1, 2, 3 и так далее. Треугольник 1 определяется точками P1, P2, P3, треугольник 2 - P2, P3, P4. Таким образом, N точек, находящихся в вертекс-буфере, соответствуют (N-2) треугольникам. Все очень хорошо, но есть и недостатки: все треугольники получаются связаны друг с другом. Вот почему четные ряды триангулируются слева направо, а нечетные - наоборот. Думаю, нет необходимости напоминать, что всякого рода нумерации у меня начинаются с нуля.

Реализация всего этого находится в методе C3DGraphic::RecalculateData(). Эта функция использует вспомогательный класс CGraphGrid, который обеспечивает построение сетки графика функции.

Управление светом в Direct3D8.

Direct 3D имеет довольно мощные средства для работы с освещением 3D-сцены. Поддерживается несколько различных типов источников света: параллельный (directional), точечный (point-source) и прожектор (spotlight). Параллельный свет не имеет источника - только направление. В качестве аналогии приведу солнце: все его лучи параллельны (простите, физики и астрономы!). Точечный свет и прожектор имеют вполне определенную точку, из которой исходят все лучи. Точечный свет испускается во все стороны, а прожектор имеет строго очерченный конус распространения лучей. Для получения более подробной информации можете обратиться к DirectX 8.0a SDK. Сейчас меня больше интересует другое. Есть одна проблема: все точки (vertexes), которые принимают участие в вычислении освещенности сцены должны включать вектор нормали. Определение нормали на самом деле очень просто, если Вы все еще помните курс школьной геометрии. Как это сделать? Есть, как минимум, 2 пути:

  • Во-первых, мы можем найти аналитическое выражение, описывающее координаты вектора нормали. Это будет очень точный результат, но для каждого новой 3D-функции придется провести все выкладки заново.
  • Во-вторых, мы можем рассчитать примерные координаты нормали исходя из координат точки, в которой определяется нормаль и координат соседних с ней точек. Пусть это лишь приближенное решение: его вполне достаточно для наших целей. К тому же оно зато абсолютно не зависит от самой функции. Именно этот подход реализован в демо-приложении и Вы можете найти его в функции C3DGraphic::CalcNormal(). Попробую прокомментировать сам алгоритм.
Рисунок: расчет нормали
- Находим 4 вектора к соседним точкам:
V01 = P1 - P0;
V02 = P2 - P0;
V03 = P3 - P0;
V04 = P4 - P0;

- Находим 4 нормали к каждой из треугольных граней.
Нормали находятся как векторное произведение соответствующих векторов.
N1 = [V02, V01];
N2 = [V03, V02];
N3 = [V04, V03];
N4 = [V01, V04];

- Искомая нормаль определяется как средний вектор четырех ранее найденных
нормалей.
N = (N1 + N2 + N3 + N4) / 4;
Управление материалами.

Все в этом мире имеет цвет. Цвет определяет восприятие нами окружающего мира. Яблоко - красное, небо - синее и так далее. Для обозначения свойств поверхности объектов Direct3D использует термин "материал". Свойства материала описываются структурой D3DMATERIAL8:

typedef struct _D3DMATERIAL8 {
    D3DCOLORVALUE   Diffuse;
    D3DCOLORVALUE   Ambient;
    D3DCOLORVALUE   Specular;
    D3DCOLORVALUE   Emissive;
    float           Power;
} D3DMATERIAL8;

Переменные Diffuse, Ambient и Specular определяют, как данный материал отражает соответствующие компоненты источников света. Кроме того, Вы можете указать мощность, с которой отражается зеркальная (specular) составляющая света - это определяет вид бликов на объектах. Ненулевая излучательная (emissive) компонента заставляет объект светится, но помните, что свет, излученный объектом, никак не отражается прочими объектами сцены. Только источники света имеют право освещать кого-либо.

Для установки свойств материала объекта Вы должны вызвать функцию SetMaterial() перед вызовом DrawPrimitive() или любой другой функции рендеринга. Делается это, например, вот так:

    hr = m_p3DDevice->SetMaterial(&m_theGraphMaterial);
    ATLASSERT(SUCCEEDED(hr));
    if (FAILED(hr)) {
        return hr;
    }

За подробностями обращайтесь к исходному коду моего проекта. Экспериментируйте, меняйте параметры, пишите, если что-то непонятно.

Вместо заключения

В конце я бы хотел сказать еще несколько слов по поводу демо-приложения. Оно имеет 4 окна свойств, каждое из которых может быть активировано из меню "Properties". Коротко опишу назначение каждого из окон:

  • Material properties. Это окошко позволяет изменить свойства материала поверхности функции: диффузионную (diffuse), окружающую (ambient), излучательную (emissive) и зеркальную (specular) компоненты, а также мощность отражения (specular power).
  • Light properties. Сцена освещена одним параллельным источником света. Вы можете изменить любую из составляющих спектра света. Кроме того, можно скорректировать направление света.
  • Background color. Это всего-навсего цвет, используемый для очистки каждого нового кадра. Вы можете выбрать любой цвет фона по Вашему усмотрению.
  • Function type. Вы можете выбрать одну из трех функций: Splash-функцию, плоскость или параболоид.

Все значения в окнах свойств редактируются с помощью трэкбаров. 0 - минимальное значение, 1 - максимальное. Минимальному значению соответствует нижнее положение трэкбара, максимальному - верхнее.

ПРИМЕЧАНИЕ
DirectX, Direct3D, Windows, Microsoft являются торговыми марками компании Microsoft. Все права защищены. OpenGL является торговой маркой фирмы Silicon Graphics Inc. Все права защищены.

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

Почему вместо нормального контекстного меню появляется узкая полоска?

Автор: Александр Шаргин

Обычно такая проблема возникает, когда вы пытаетесь выполнить код следующего вида:

    POINT pt;
    GetCursorPos(&pt);

    HMENU hMenu;
    hMenu = LoadMenu(hInstance, MAKEINTRESOURCE(IDR_MENU1));
    TrackPopupMenu(hMenu, 0, pt.x, pt.y, 0, hWnd, NULL);
    DestroyMenu(hMenu);

В чём же здесь ошибка? Дело в том, что в Windows существует два совершенно разных вида меню - полоска меню (menu bar), которая традиционно размещается под заголовком окна, и всплывающее меню (popup menu). Работа и с тем, и с другим осуществляется с помощью хэндла типа HMENU. Это вносит некоторую путаницу, так как функции, предназначенные для работы с всплывающим меню, не могут работать с полоской меню, и наоборот.

Дескриптор всплывающего меню возвращают всего две функции - CreatePopupMenu и GetSubMenu. Именно эти функции можно использовать совместно с TrackPopupMenu(Ex). С другой стороны, функция LoadMenu загружает из ресурсов полоску меню, что и приводит к ошибке.

Описание и примеры использования функций CreatePopupMenu и GetSubMenu можно найти в статье "Как отобразить контекстное меню?".

ЭКЗАМЕН

What two rectangular regions does Windows use to derive a scaling factor and an orientation?

  1. Viewport and quadrant
  2. Window and frame
  3. Frame and viewport
  4. Quadrant and frame
  5. Window and viewport

Верный ответ - 5. Window and viewport. Именно они используются в Windows для определения координат точек и коэффициента масштабирования.

Это все на сегодня. Пока!

Алекс Jenter jenter@rsdn.ru
Duisburg, 2001. Публикуемые в рассылке материалы принадлежат сайту RSDN.


ПРОГРАММИРОВАНИЕ НА V I S U A L C + +
РАССЫЛКА САЙТА
RSDN.RU
Выпуск No. 53 от 4 ноября 2001 г.

РАССЫЛКА ЯВЛЯЕТСЯ ЧАСТЬЮ ПРОЕКТА RSDN , НА САЙТЕ КОТОРОГО ВСЕГДА МОЖНО НАЙТИ ВСЮ НЕОБХОДИМУЮ РАЗРАБОТЧИКУ ИНФОРМАЦИЮ, СТАТЬИ, ФОРУМЫ, РЕСУРСЫ, ПОЛНЫЙ АРХИВ ПРЕДЫДУЩИХ ВЫПУСКОВ РАССЫЛКИ И МНОГОЕ ДРУГОЕ.

Приветствую вас, дорогие подписчики!

Прежде всего хочу извиниться - в предыдущем выпуске в статье про Direct3D по моему недосмотру были указаны некорректные ссылки на примеры программ. Ну, все мы люди ;-) Вот верные ссылки:

Демонстрационное приложение (только .exe) (72 kb)
Демонстрационное приложение (исходный код) (44 kb)

Также ссылки исправлены в той версии выпуска, которая лежит в архиве на сайте RSDN.

И еще один вопрос: мне продолжают приходить письма с различными вопросами по программированию. К сожалению, у меня сейчас нет времени даже на то, чтобы просто отвечать на все эти письма, не говоря уже о содержащихся в них вопросах. Поэтому хочу всем напомнить, что в ФОРУМЕ на сайте RSDN вы можете получить ответ (и даже не один!) на любой свой вопрос по программированию.

Поэтому большая просьба вопросы задавать там, вам наверняка ответят. А если вы считаете какой-нибудь из вопросов очень интересным и достойным внимания подписчиков этой рассылки, то я буду благодарен, если вы мне пришлете ссылку на соответствующее сообщение форума. Дело в том, что у рассылки начиная с этого выпуска появляется новая рубрика - "ФОРУМ RSDN - ИЗБРАННОЕ", и там будут публиковаться самые интересные дискуссии. Надеюсь, это сделает рассылку для вас еще интереснее.

CТАТЬЯ

Подключение к событиям объектной модели DHTML при использовании WebBrowser-control
Автор: Тимофей Чадов

Демонстрационное приложение - event.zip (181 Kb)

Введение

В последнее время путешествуя по форумам и группам новостей все чаще можно встретить вопросы, касающиеся ActiveX-элемента WebBrowser. К сожалению зачастую эти вопросы теряются в общем ворохе сообщений, а материалов касающихся использования функциональности Internet Exploler катастрофически мало. В то же время у этих технологий находиться все больше поклонников. Уж слишком заманчиво выглядит возможность написать собственный броузер, приложив минимум усилий.

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

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

Немного теории

Как и большинство ActiveX элементов WebBrowser является источником событий подключаемых через стандартный механизм Connection Point. К числу таких событий относятся OnBeforeNavigate, OnDocumentComplete и т.п. Несомненно они важны для управления приложением в целом, однако их возможностей явно недостаточно, если мы захотим более тесно познакомиться с DOM DHTML, например, узнать о перемещении мыши над элементами страницы или о нажатии клавиши на клавиатуре или вообще быть в курсе всех событий, которые можно использовать в сценариях DHTML.

При использовании объектной модели DHTML из сценариев возможно создавать собственные обработчики событий простым присваиванием наблюдаемого элемента соответствующим свойствам, например:

<SCRIPT LANGUAGE="jscript">
function mousedownhandler()
{
    // функция обработчик
}

function afterPageLoads()
{
    someElement.onmousedown = mousedownhandler;
}
</SCRIPT>

Возникает вопрос. А можно ли получить нечто подобное из клиента на С++? Конечно, причем похожим образом. Достаточно создать свою функцию обработки и зарегистрировать ее.

Теперь поподробнее. Для этого нужно проделать не так уж и много. Необходимо реализовать простой Com-объект для каждой функции обработчика. Этот объект должен реализовывать всего два стандартных интерфейса IUnknown и IDispatch. Далее ссылка на этот объект присваивается соответствующему свойству элемента, событие которого мы хотим наблюдать. При возникновении события броузер просто вызовет IDispatch::Invoke нашего объекта со значением DISPID = DISPID_VALUE (=0).

Замечу, что это не единственный способ заставить приложения реагировать на события. Например, можно заставить работать механизм window.external. Тогда соответствующий скрипт будет выглядеть например, так:

function mousedownhandler()
{
    // функция обработки
    window.external.onmousedown;
}

Думаю идея понятна. Однако этот способ удобен если мы сами формируем страницу. Сегодня мы пойдем первым путем.

Пишем шаблоный класс

Итак, настало время применить все вышеизложенное на практике. Конечно, можно руками написать COM-объекты для каждой функции, однако представьте, что число обработчиков переваливает за десяток, а каждый раз нужно реализовывать по сути одно и тоже. Думаю, понятно, к чему я клоню :) Самое время вспомнить о шаблонах С++. Итак, напишем простенький шаблонный класс. Чтобы не зависеть от конкретной библиотеки реализуем пару IUnknown, IDispatch вручную. Реализация IUnknown вполне стандартна, а из IDispatch необходимо реализовать только функцию Invoke.

template <class T> class CHtmlEventObject : public IDispatch
{

typedef void (T::*EVENTFUNCTIONCALLBACK)(DISPID id, VARIANT* pVarResult);

public:
   CHtmlEventObject() { m_cRef = 0; }
   ~CHtmlEventObject() {}

   HRESULT __stdcall QueryInterface(REFIID riid, void** ppvObject)
   {
      *ppvObject = NULL;

      if (IsEqualGUID(riid, IID_IUnknown))
         *ppvObject = reinterpret_cast<void**>(this);

      if (IsEqualGUID(riid, IID_IDispatch))
         *ppvObject = reinterpret_cast<void**>(this);

      if (*ppvObject)
      {
          ((IUnknown*)*ppvObject)->AddRef();
          return S_OK;
      }
      else return E_NOINTERFACE;
   }

   DWORD __stdcall AddRef()
   {
      return InterlockedIncrement(&m_cRef);
   }

   DWORD __stdcall Release()
   {
      if (InterlockedDecrement(&m_cRef) == 0)
      {
         delete this;
         return 0;
      }
      return m_cRef;
   }

   STDMETHOD(GetTypeInfoCount)(unsigned int FAR* pctinfo)
   { return E_NOTIMPL; }

   STDMETHOD(GetTypeInfo)(unsigned int iTInfo, LCID  lcid, ITypeInfo FAR* FAR*  ppTInfo)
   { return E_NOTIMPL; }

   STDMETHOD(GetIDsOfNames)(REFIID riid, OLECHAR FAR* FAR* rgszNames, unsigned int cNames,
                            LCID lcid, DISPID FAR* rgDispId)
   { return S_OK; }

   STDMETHOD(Invoke)(DISPID dispIdMember, REFIID riid, LCID lcid,
      WORD wFlags, DISPPARAMS* pDispParams, VARIANT* pVarResult,
      EXCEPINFO * pExcepInfo, UINT * puArgErr)
   {
      if (DISPID_VALUE == dispIdMember)
         (m_pT->*m_pFunc)(m_id, pVarResult);
      else
         TRACE(_T("Invoke dispid = %d\n"), dispIdMember);

      return S_OK;
   }

public:
   static LPDISPATCH CreateHandler(T* pT,
                        EVENTFUNCTIONCALLBACK pFunc, DISPID id)
   {
      CHtmlEventObject<T>* pFO = new CHtmlEventObject<T>;
      pFO->m_pT = pT;
      pFO->m_pFunc = pFunc;
      pFO->m_id = id;
      return reinterpret_cast<LPDISPATCH>(pFO);
   }

protected:
   T* m_pT;
   EVENTFUNCTIONCALLBACK m_pFunc;
   DISPID m_id;
   long m_cRef;
};

Как применять этот класс? Проще простого.

Шаг 1. Создаем свою функцию обработчик по прототипу OnEvent(DISPID id, VARIANT* pVarResult). В принципе ее можно разместить где угодно. Я предпочитаю создавать ее в классе представления, наследнике CHtmlView. При этом все обработчики сосредоточены в одном месте и не нужно беспокоится о взаимодействии с классом документа.

Шаг 2. Регистрируем ее в качестве обработчика интересующего нас события. Для этого через вызов CHtmlEventObject::CreateObject создаем экземпляр нашего COM-объекта. Передаем в него адрес функции обработчика и собственный идентификатор события. После этого передаем ссылку на него интересующему нас элементу.

// Создаем объект-обработчик
LPDISPATCH dispFO = CHtmlEventObject<CEventView>::CreateHandler(this, OnKeyDown, 1);
VARIANT vIn;
V_VT(&vIn) = VT_DISPATCH;
V_DISPATCH(&vIn) = dispFO;

// устанавливаем обработчик document.onkeydown
hr = pHtmlDoc->put_onkeydown( vIn );

Здесь есть одна тонкость. Зарегистрировать обработчик можно только тогда, когда документ уже загружен, иначе GetHtmlDocument() вернет NULL. Для этого можно отслеживать событие OnDocumentComplete. Ну вот собственно и все.

Получаем информацию о событии

Рассмотрим еще раз прототип функции обработчика

OnEvent(DISPID id, VARIANT* pVarResult);

В качестве id передается значение которое мы указали при регистрации обработчика. Это может пригодиться если мы захотим реализовать обработку нескольких событий в одной функции. Для этого нужно зарегистрировать одну и ту же функцию с разными DISPID, тогда по приходу событий мы сможет их различать.

pVarResult нужно, если не требуется обработки по умолчанию.При этом достаточно в pVarResult вернуть VARIANT_FALSE.

Итак, когда вызывается наш обработчик никакой дополнительной информации о событии в функцию не передается. А как же тогда поподробнее узнать, что произошло? Для этого необходимо воспользоваться интерфейсом IHTMLEventObj, доступным через объект window текущего документа. Посредством этого интерфейса можно получить подробную информацию о произошедшем событии, например, элемент, послуживший источником событий, состояние клавиш, местоположение курсора мыши и состояние ее кнопок.

Вот его краткое описание из MSDN:

Методы IHTMLEventObj

get_altKey | Состояние клавиши Alt
get_button | Возвращает информацию о нажатых кнопках мыши
get_cancelBubble | Возвращает будет ли продолжена обработка события вверх по иерархии обработчиков
get_clientX | Возвращает горизонтальную позицию курсора мыши относительно клиентской области окна
get_clientY | Возвращает вертикальную позицию курсора мыши относительно клиентской области окна
get_ctrlKey | Состояние клавиши Ctrl
get_fromElement | Возвращает указатель на интерфейс IHTMLElement позволяющий получить доступ к элементу с которого "ушел" курсор мыши при событиях onmouseover или onmouseout.
get_keyCode | Возвращает код нажатой клавиши
get_offsetX | Возвращает горизонтальную позицию курсора относительно контейнера элемента
get_offsetY | Возвращает позицию курсора относительно контейнера элемента
get_qualifier | Возвращает идентификатор события
get_reason | Возвращает состояние передачи данных для объекта источника данных
get_returnValue | Возвращаемое значение события или диалога
get_screenX | Горизонтальная координата относительно координат экрана
get_screenY | Вертикальная координата относительно координат экрана
get_shiftKey | Состояние клавиши Shift
get_srcElement | Возвращает указатель на интерфейс IHTMLElement послуживший источником событий
get_srcFilter | Возвращает объект фильтр возбудивший событие onfilterchange
get_toElement | Возвращает указатель на интерфейс IHTMLElement позволяющий получить доступ к элементу с на который "пришел" курсор мыши при событиях onmouseover или onmouseout
get_type | Возвращает строковое название события
get_x | Возвращает горизонтальную позицию мыши относительно родительского объекта в иерархии, позиционированного с помощью атрибутов CSS
get_y | Возвращает вертикальную позицию мыши относительно родительского объекта в иерархии, позиционированного с помощью атрибутов CSS
put_cancelBubble | Задать будет ли продолжена обработка события вверх по иерархии обработчиков
put_keyCode | Задать код нажатой клавиши
put_returnValue | Задать возвращаемое событием значение

Стоит заметить, что интерфейс IHTMLEventObj доступен только на время обработки конкретного события. При этом не все свойства в контексте определенного события имеют смысл. Например, значения возвращаемые функциями get_fromElement и get_toElement доступны только при обработке событий мыши onmouseover и onmouseout.

В следующем примере в обработчике обределяется нажатая клавиша и выводится соответствующее диалоговое окно. Если была нажата клавиша Enter, то дальнейшая обработка отменяется.

void CMyHtmlView::OnKeyDown( DISPID id, VARIANT* pVarResult)
{
    HRESULT hr;
    LPDISPATCH pDispatch = GetHtmlDocument();
    if( pDispatch != NULL )
    {
        IHTMLDocument2* pHtmlDoc;
        hr = pDispatch->;
        QueryInterface( __uuidof( IHTMLDocument2 ), (void**)&pHtmlDoc );

        IHTMLWindow2*  pWindow;
        IHTMLEventObj* pEvent;

        hr = pHtmlDoc->get_parentWindow(&pWindow);
        ASSERT( SUCCEEDED( hr ) );
        hr = pWindow->get_event(&pEvent);
        ASSERT( SUCCEEDED( hr ) );

        // Определяем нажатую клавишу
        long nKey;
        hr = pEvent->get_keyCode( &nKey );
        ASSERT( SUCCEEDED( hr ) );

        // Если Enter не хотим обрабатывать дальше
        if ( nKey == VK_RETURN)
        {
            V_VT(pVarResult) = VT_BOOL;
            V_BOOL(pVarResult) = FALSE;
        }

        pDispatch->Release();
        pWindow->Release();
        pEvent->Release();
        pHtmlDoc->Release();

        CString sMes;
        sMes.Format("CEventView::OnKeyDown(DISPID = %d)\nKeyCode: %d", id, nKey);
        AfxMessageBox(sMes);
    }
}
В заключение

Чтобы собрать воедино все фрагменты приведу небольшой пример (event.zip). Запустите его и выберите команду меню Event\OnKeydown. Теперь понажимайте клавиши внутри страницы. И посмотрите, что из этого получится. В этом примере регистрируется только один обработчик, но я думаю дочитав эту статью вы без труда сможете реализовать любой другой.

В заключение хочется заметить, что в этой статье я затронул только один небольшой аспект использования элемента WebBrowser. Если Вас заинтересует данная тема, пишите, продолжим.

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

Как получить список запущенных приложений?
Автор: Александр Федотов

Тестовое приложение Process Viewer - pview.zip (130 Kb)

Операционная система Windows знаменита своими окнами. Поэтому, когда заходит речь о приложениях, пользователи ассоциируют их с окнами, хотя с технической точки зрения правильнее было бы оперировать понятием процесса. В частности, под списком приложений пользователи часто подразумевают не список процессов, а список главных окон приложений, видимых на экране. В таком случае, задача перечисления приложений сводится к перечислению их главных окон.

Для перечисления всех окон верхнего уровня служит функция EnumWindows, однако, если мы просто вызовем эту функцию, то обнаружим, что она возвращает много больше окон, чем видно приложений на экране. Очевидно, мы должны игнорировать невидимые окна и окна, имеющие владельца (такие как диалоговые панели). Но даже и после этой фильтрации в списке будет несколько больше окон, чем отображает Task Manager.

Проведя несколько экспериментов с Windows NT Task Manager, удалось установить, что он игнорирует окна с пустым заголовком, и, как ни странно, окна с заголовком "Program Manager". Да-да, если вы создатите свое окно с таким заголовком, то оно не появится в списке приложений Task Manager. Обычно в системе есть только одно окно "Program Manager" - это то окно, на котором находится рабочий стол. Понятно, что пользователи не ассоциируют это окно с каким-либо приложением, для них оно является неотъемлемой частью компьютера, и поэтому Task Manager должен игнорировать это окно. Непонятно только, почему разработчики Task Manager решили определять это окно по его заголовку, а не по имени класса окна, которое есть "progman".

После того, как мы вставим все эти проверки, наш список уже ничем не будет отличаться от выводимого Task Manager. Ниже приведен код функции EnumApplications, которая реализует перечисление приложений. Интерфейс функции построен в стиле функций-перечислителей в Win32 API: она принимает указатель на пользовательскую функцию, которую вызывает для каждого перечисляемого приложения.

typedef BOOL (CALLBACK * PFNENUMAPP)(
    IN HWND hWnd,             // идентификатор главного окна приложения
    IN LPCTSTR pszName,       // название приложения
    IN HICON hIcon,           // иконка приложения
    IN LPARAM lParam          // пользовательский параметр
    );

typedef struct _ENUMAPPDATA {
    LPARAM        lParam;
    PFNENUMAPP    pfnEnumApp;
} ENUMAPPDATA, * PENUMAPPDATA;

static
BOOL CALLBACK EnumWindowsCallback(
    IN HWND hWnd,
    IN LPARAM lParam
    )
{
    PENUMAPPDATA pEnumData = (PENUMAPPDATA)lParam;
    _ASSERTE(_CrtIsValidPointer(pEnumData, sizeof(ENUMAPPDATA), 1));

    if (!IsWindowVisible(hWnd) || GetWindow(hWnd, GW_OWNER) != NULL)
        return TRUE;

    TCHAR szClassName[80];
    GetClassName(hWnd, szClassName, 80);

    if (lstrcmpi(szClassName, _T("Progman")) == 0)
        return TRUE;

    // получаем заголовок окна
    TCHAR szText[256];
    DWORD cchText = GetWindowText(hWnd, szText, 256);

    if (cchText == 0)
        return TRUE;

    HICON hIcon = NULL;

    // получаем иконку окна
    if (SendMessageTimeout(hWnd, WM_GETICON, ICON_SMALL, 0,
                            SMTO_ABORTIFHUNG|SMTO_BLOCK, 1000,
                            (DWORD_PTR *)&hIcon))
    {
        if (hIcon == NULL)
        {
            if (!SendMessageTimeout(hWnd, WM_GETICON, ICON_BIG, 0,
                                SMTO_ABORTIFHUNG|SMTO_BLOCK, 1000,
                                (DWORD_PTR *)&hIcon))
                hIcon = NULL;
        }
    }
    else
        hIcon = NULL;

    if (hIcon == NULL)
        hIcon = (HICON)GetClassLong(hWnd, GCL_HICONSM);
    if (hIcon == NULL)
        hIcon = (HICON)GetClassLong(hWnd, GCL_HICON);
    if (hIcon == NULL)
        hIcon = LoadIcon(NULL, IDI_APPLICATION);

    // вызываем пользовательскую функцию
    return pEnumData->pfnEnumApp(hWnd, szText, hIcon, pEnumData->lParam);
}

BOOL EnumApplications(
    IN PFNENUMAPP pfnEnumApp,
    IN LPARAM lParam
    )
{
    _ASSERTE(pfnEnumApp != NULL);

    ENUMAPPDATA EnumData;
    EnumData.pfnEnumApp = pfnEnumApp;
    EnumData.lParam = lParam;

    return EnumWindows(EnumWindowsCallback, (LPARAM)&EnumData);
}

Как видно, функция EnumApplications чрезвычайно проста - она просто вызывает EnumWindows и вся основная работа по фильтрации ненужных окон ложится на вспомогательную функцию EnumWindowsCallback.

В функции EnumWindowsCallback мы сначала отсеиваем невидимые окна, и окна, имеющие владельца. Затем мы проверяем, не является ли данное окно окном рабочего стола. Здесь мы не уподобляемся разработчикам Windows NT Task Manager и используем имя класса окна для проверки. Наконец, мы отбрасываем окна с пустым заголовком.

После того, как мы определи, что данное окно представляет некоторое приложение, мы собираем информацию об окне, чтобы передать ее пользовательской функции. Сначала мы получаем заголовок окна с помощью хорошо известной функции GetWindowText. Затем мы пытаемся получить иконку окна. Обратите внимание, мы используем функцию SendMessageTimeout с флагом SMTO_ABORTIFHUNG для посылки сообщения WM_GETICON. Это гарантирует, что наше приложение не зависнет, даже если приложение, которому принадлежит окно, перестало обрабатывать сообщения.

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

Cсылки
  1. Q175030 HOWTO: Enumerate Applications in Win32, Microsoft Knowledge Base.

ФОРУМ RSDN - ИЗБРАННОЕ

Тема: ООП и наследование

Utandr:

Вопрос:
  Есть базовый класс, из ЕГО конструктора вызывается метод ЭТОГО же (базового)
  класса.

Cbase::Cbase()
{
someFunction();
}

void Cbase::someFunction()
{
  <SOME ACTION>
}

Так вот. Этот класс наследуют другие классы. НО! В их конструкторах нет вызова
someFunction(), - КОТОРАЯ ВИРТУАЛЬНАЯ, и по логике должна _переназначаться_
классами, которые наследуют Cbase

Но при объявлении Csomefrombase : public Cbase, при объявлении объекта
вызывается someFunction() класса Cbase! Но нужно, чтобы вызывалась
ТОЛЬКО Csomefrombase::someFunction()!

Кто-нибудь подскажет, как решить данную проблему?

IT:

Предположим, всё работает по твоей логике. Угадай, что произойдёт вот в таком случае?

class A {
public:
    A() { f(); }
    virtual void f() {}
};

class B: public A {
public:
    int *n;
    B() { n = new int[10]; }
};

class C: public B {
public:
    virtual void f()
    {
        for (int i=0; i<10; i++)
            n = 0;
    }
};

Андрей Тарасевич:

Из конструктора даже виртуальные методы вызываются в соостветствии со
статическим (а не динамическим) типом класса. Т.е. при вызове метода 'foo()' из
конструктора класса 'A' всегда вызывается метод 'A::foo()', независимо от того,
переопределялся ли это метод в наследнике или нет. В этом есть смысл: в момент
выполнения конструктора базового класса класс-наследник еще не сконструировался
и попытки вызывать его методы ни к чему хорошему не приведут (см. пример от IT)

IT:

Всё правильно, но я бы хотел уточнить некоторые детали реализации. На самом деле
функции вызываются как обычно, т.е. по всем правилам вызова виртуальных функций
через VMT. Если бы даже конструктор и вызывал виртуальные функции по другому, то
его можно было бы легко обмануть, вызвав из него любую функцию из которой уже
вызвать виртуальную.
Всё гораздо проще. указатель на VMT инициализируется,
как известно, в самом начале работы конструктор, но ПОСЛЕ вызова конструктора
базового класса. Поэтому, каждый конструктор устанавливает VMT на своём уровне и
здесь его уже никак не обмануть. С деструкторами ситуация такая же, только в
обратном порядке.

Андрей Тарасевич:

Все это действительно детали реализации. В данном конкретном случае естественный
порядок инициализации указателя на VMT как нельзя лучше способствует соблюдению
спецификации языка. Создатели компиляторов знают об этой спецификации и,
например, в MSVC++ виртуальные методы при прямом вызове их из конструктора
вызываются статически, а не виртуально (т.е. в общем случае ты не прав, говоря,
что "на самом деле функции вызываются как обычно"). А вот если попытаться сделать
непрямой вызов виртуального метода (через метод-последник), то тут уже действительно
срабатывает "правильное" значение указателя на VMT.

Геннадий Васильев:

U>> Кто-нибудь подскажет, как решить данную проблему?

Разделить создание объекта и конструирование, например, ввести метод Init(),
вызываемый после конструктора.

class A
{
    int field;
  public:
    A(void) : field(0){}
    virtual ~A(void){}
    virtual bool Init(int val)
    {
      // Здесь - дополнительная инициализация
      field = val;
      return true;
    }
};

// Ну и, собственно, наследник
class B : public A
{
  public:
    virtual bool Init(int val);
};

// Фрагмент программы:
A *pa = new B; // Это - не опечатка! ;)
if (pa)
{
  // к моменту вызова Init объект B уже создан,
  // его VMT нициализирована, бояться нечего :))
  if (!pa -> Init(123))
  {
    delete pa;
    pa = NULL;
  }
}

В принципе, можно поступить поизящней -
уложить подобные действия в шаблонную функцию, как например в ATL реализована
CComObject::CreateInstance.

АТ>Из конструктора даже виртуальные методы вызываются в
соостветствии со статическим (а не динамическим) типом класса. Т.е. при вызове
метода 'foo()' из конструктора класса 'A' всегда вызывается метод 'A::foo()',
независимо от того, переопределялся ли это метод в наследнике или нет. В этом
есть смысл: в момент выполнения конструктора базового класса класс-наследник еще
не сконструировался и попытки вызывать его методы ни к чему хорошему не приведут
(см. пример от IT)
А кроме того - можно попасться на вызове чисто
виртуального метода, когда указатель на него еще не установлен. Например - так:

class C
{
  public:
    C(void){ Init(); }
    void Init(void)
    {
      InitExtra(); // Вызываем виртуальную функцию
    }
    virtual void InitExtra(void) = 0; // sic! pure virtual!
};

class D : public C
{
  public:
    D(void){}
    // Определим функцию InitExtra
    virtual void InitExtra(void)
    { /* что-то содержательное */ }
};

// Фрагмент программы:
C *pc = new C; // Здесь компилятор выругается по поводу
               // недопустимости создания абстракного класса.

D *pd = new D; // А здесь компилятор все пропустит.
               // Но! см. ниже...

При создании объекта класса D первым,
как полагается, будет создан объект класса C. В его конструкторе будет вызван
статический метод Init, который, в свою очередь, вызовет виртуальный InitExtra,
и вылетит с ошибкой Pure virtual call или что-то вроде этого, поскольку в VMT
класса C на месте InitExtra находится 0 (вернее - вызов обработчика аварийной
ситуации), а VMT наследника еще не создан.

Это все на сегодня. До встречи!

Алекс Jenter jenter@rsdn.ru
Duisburg, 2001. Публикуемые в рассылке материалы принадлежат сайту RSDN.


ПРОГРАММИРОВАНИЕ НА V I S U A L C + +
РАССЫЛКА САЙТА
RSDN.RU
Выпуск No. 54 от 11 ноября 2001 г.

РАССЫЛКА ЯВЛЯЕТСЯ ЧАСТЬЮ ПРОЕКТА RSDN , НА САЙТЕ КОТОРОГО ВСЕГДА МОЖНО НАЙТИ ВСЮ НЕОБХОДИМУЮ РАЗРАБОТЧИКУ ИНФОРМАЦИЮ, СТАТЬИ, ФОРУМЫ, РЕСУРСЫ, ПОЛНЫЙ АРХИВ ПРЕДЫДУЩИХ ВЫПУСКОВ РАССЫЛКИ И МНОГОЕ ДРУГОЕ.

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

Я наконец разобрался с CHM-файлом архива рассылки, так что теперь там поиск должен работать нормально (правда, как и раньше, только для латиницы). Новый файл, в который я заодно добавил вышедшие за это время выпуски, можно скачать здесь.

Сегодня в выпуске - долгожданное продолжение руководства по WTL Александра Шаргина. К сожалению из-за большого объема статьи, которую даже пришлось разбить на две части, остальных рубрик сегодня не будет. Но статья того без сомнения стоит!

CТАТЬЯ

Использование WTL
Часть 2. Диалоги и контролы
Автор: Александр Шаргин
Диалоги

Диалоговые окна широко используются в Windows-приложениях, начиная с момента выхода самой операционной системы Windows. Они очень удобны для организации диалога с пользователем (отсюда их название). Кроме того, в несложных приложениях часто удаётся построить на базе диалогов не только вспомогательные окна, но и главное окно приложения (такие приложения иногда называют "dialog-based"). В этом разделе мы рассмотрим классы WTL, предназначенные для работы с диалоговыми окнами.

Классы WTL для работы с диалогами

Классы WTL, относящиеся к диалоговым окнам, показаны на рисунке 1.

Рисунок 1. Диалоговые классы WTL

Обратите внимание, что все диалоговые классы порождаются от базового класса CWindowImplRoot<>, а не от класса CWindowImpl<>. Это сделано потому, что диалоги, в отличие от всех остальных окон, не используют оконную процедуру для обработки сообщений. Вместо этого используется диалоговая процедура, адрес которой задаётся при создании диалога. WTL предоставляет вам свою реализацию диалоговой процедуры в классе CDialogImplBaseT<>. Соответственно, все остальные классы диалогов WTL наследуют эту реализацию.

ПРИМЕЧАНИЕ
Все классы, показанные на рисунке 1, WTL унаследовала от библиотеки ATL. Они описаны в файле atlwin.h

Теперь изучим каждый класс более подробно.

Класс CDialogImplBaseT<>

Итак, класс CDialogImplBaseT<> содержит функциональность, необходимую всем без исключения диалоговым окнам. Это, в первую очередь, поддержка диалоговых процедур, а также пара вспомогательных функций. Обратите внимание, что в класс CDialogImplBaseT<> не встроен механизм создания диалога при помощи функций DialogBox и CreateDialog. Дело в том, что не все диалоги нуждаются в этих функциях. Например, стандартные диалоги создаются при помощи специальных функций (GetOpenFileName, ChooseColor и т. д.).

Диалоговые процедуры в классе CDialogImplBaseT<> реализованы более или менее аналогично оконным процедурам в классе CWindowImplBaseT<>.

template <class TBase>
LRESULT CALLBACK
CDialogImplBaseT< TBase >::StartDialogProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    CDialogImplBaseT< TBase >* pThis = (CDialogImplBaseT< TBase >*)_Module.ExtractCreateWndData();
    ATLASSERT(pThis != NULL);

    pThis->m_hWnd = hWnd;
    pThis->m_thunk.Init(pThis->GetDialogProc(), pThis);

    WNDPROC pProc = (WNDPROC)&(pThis->m_thunk.thunk);
    WNDPROC pOldProc = (WNDPROC)::SetWindowLong(hWnd, DWL_DLGPROC, (LONG)pProc);

#ifdef _DEBUG
    // check if somebody has subclassed us already since we discard it
    if(pOldProc != StartDialogProc)
        ATLTRACE2(atlTraceWindowing, 0, _T("Subclassing through a hook discarded.\n"));
#else
    pOldProc;    // avoid unused warning
#endif

    return pProc(hWnd, uMsg, wParam, lParam);
}

template <class TBase>
LRESULT CALLBACK
CDialogImplBaseT< TBase >::DialogProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    CDialogImplBaseT< TBase >* pThis = (CDialogImplBaseT< TBase >*)hWnd;

    // set a ptr to this message and save the old value
    MSG msg = { pThis->m_hWnd, uMsg, wParam, lParam, 0, { 0, 0 } };
    const MSG* pOldMsg = pThis->m_pCurrentMsg;
    pThis->m_pCurrentMsg = &msg;

    // pass to the message map to process
    LRESULT lRes;
    BOOL bRet = pThis->ProcessWindowMessage(pThis->m_hWnd, uMsg, wParam, lParam, lRes, 0);

    // restore saved value for the current message
    ATLASSERT(pThis->m_pCurrentMsg == &msg);
    pThis->m_pCurrentMsg = pOldMsg;

    // set result if message was handled
    if(bRet)
    {
        switch (uMsg)
        {
        case WM_COMPAREITEM:
        case WM_VKEYTOITEM:
        case WM_CHARTOITEM:
        case WM_INITDIALOG:
        case WM_QUERYDRAGICON:
        case WM_CTLCOLORMSGBOX:
        case WM_CTLCOLOREDIT:
        case WM_CTLCOLORLISTBOX:
        case WM_CTLCOLORBTN:
        case WM_CTLCOLORDLG:
        case WM_CTLCOLORSCROLLBAR:
        case WM_CTLCOLORSTATIC:
            return lRes;
            break;
        }
        ::SetWindowLong(pThis->m_hWnd, DWL_MSGRESULT, lRes);
        return TRUE;
    }

    if(uMsg == WM_NCDESTROY)
    {
        // clear out window handle
        HWND hWnd = pThis->m_hWnd;
        pThis->m_hWnd = NULL;
        // clean up after dialog is destroyed
        pThis->OnFinalMessage(hWnd);
    }

    return FALSE;
}

Статическая функция StartDialogProc назначается диалогу при его создании. Для этого её адрес передаётся функциям, подобным DialogBox и CreateDialog, или задаётся в качестве хука для стандартных диалогов. Получив управление, эта функция извлекает хэндл диалога из объекта _Module и сохраняет его в переменной m_hWnd, затем инициализирует переходник и передаёт управление штатной диалоговой процедуре DialogProc, которая и выполняет дальнейшее обслуживание диалога. Каждое полученное сообщение она "пропускает" через карту сообщений вызовом ProcessWindowMessage. Возвращаемое после обработки сообщения значение интерпретируется в зависимости от типа сообщения. Тем самым обеспечивается небольшое, но весьма приятное удобство: программист не должен помнить, каким образом нужно передать операционной системе LRESULT из диалоговой процедуры (напрямую или с помощью SetWindowLong). Достаточно вернуть его из функции-обработчика, а об остальном позаботится WTL.

Поскольку диалоги, как и все остальные окна, используют для обработки сообщений функцию ProcessWindowMessage, вы можете использовать для её создания уже знакомые вам макросы карты сообщений.

После уничтожения диалога WTL вызывает виртуальную функцию OnFinalMessage. Вы можете переопределить её в производном классе и возложить на неё "очистительные" работы. Следует только иметь в виду, что во время работы этой функции диалог уже не существует, и даже переменная m_hWnd содержит NULL. Поэтому в функции OnFinalMessage нельзя, к примеру, загружать данные из контролов диалога в переменные.

Класс CDialogImpl<>

Класс CDialogImpl<> - основное средство для работы с диалогами в WTL. Он используется как с модальными, так и с немодальными диалогами. Соответственно, в нём содержатся обёртки для функций DialogBoxParam, EndDialog, CreateDialogParam и DestroyWindow. Механизм обработки сообщений наследуется от класса CDialogImplBaseT<>.

Для создания модального диалога используется метод DoModal. Уничтожить модальный диалог можно, используя метод EndDialog (можно вызывать этот метод из любого обработчика сообщений, в том числе из обработчика сообщения WM_INITDIALOG). Реализация обоих методов более чем прямолинейна:

    // modal dialogs
    int DoModal(HWND hWndParent = ::GetActiveWindow(), LPARAM dwInitParam = NULL)
    {
        ATLASSERT(m_hWnd == NULL);
        _Module.AddCreateWndData(&m_thunk.cd, (CDialogImplBaseT< TBase >*)this);

#ifdef _DEBUG
        m_bModal = true;
#endif //_DEBUG
        return

        ::DialogBoxParam(_Module.GetResourceInstance(), MAKEINTRESOURCE(T::IDD),
                    hWndParent, (DLGPROC)T::StartDialogProc, dwInitParam);
    }

    BOOL EndDialog(int nRetCode)
    {
        ATLASSERT(::IsWindow(m_hWnd));
        ATLASSERT(m_bModal);    // must be a modal dialog
        return ::EndDialog(m_hWnd, nRetCode);
    }

Здесь следует обратить внимание всего на две вещи. Во-первых, в качестве диалоговой процедуры задаётся StartDialogProc. Благодаря этому к создаваемому диалогу подключается механизм обработки сообщений, рассмотренный в предыдущем разделе. Во-вторых, в качестве идентификатора ресурса диалога используется константа IDD. Вам необходимо определить её в производном классе, чтобы WTL знала, какой диалог требуется создать. В принципе, можно сделать IDD и статической переменной производного класса, но прибегать к этому приёму на практике приходится не часто.

ПРИМЕЧАНИЕ
Библиотека MFC не использует функцию DialogBox(Param). Вместо этого она создаёт немодальный диалог, а затем эмулирует поведение модального. Благодаря этому программировать модальные диалоги в MFC гораздо удобнее, чем на "чистом" Win32 API (а значит, и в WTL). Проблема в том, что функция DialogBox(Param) создаёт свой собственный цикл сообщений, до которого не так-то просто добраться. Если нам потребуется, к примеру, внедрить в него трансляцию акселераторов, придётся прибегать к различным неочевидным приёмам.

Немодальный диалог создаётся с использованием функции Create и разрушается вызовом DestroyWindow. Реализация обоих методов также достаточно очевидна.

    // modeless dialogs
    HWND Create(HWND hWndParent, LPARAM dwInitParam = NULL)
    {
        ATLASSERT(m_hWnd == NULL);
        _Module.AddCreateWndData(&m_thunk.cd, (CDialogImplBaseT< TBase >*)this);

#ifdef _DEBUG
        m_bModal = false;
#endif //_DEBUG

        HWND hWnd = ::CreateDialogParam(_Module.GetResourceInstance(), MAKEINTRESOURCE(T::IDD),
                    hWndParent, (DLGPROC)T::StartDialogProc, dwInitParam);
        ATLASSERT(m_hWnd == hWnd);
        return hWnd;
    }

    BOOL DestroyWindow()
    {
        ATLASSERT(::IsWindow(m_hWnd));
        ATLASSERT(!m_bModal);    // must not be a modal dialog
        return ::DestroyWindow(m_hWnd);
    }

С учётом всего сказанного, типичный класс диалога, порождённый от CDialogImpl<>, выглядит так (в качестве параметра шаблона задаётся имя класса, который вы порождаете).

class CMyDialog : public CDialogImpl<CMyDialog>
{
public:
    enum { IDD = IDD_MY_DIALOG };

    BEGIN_MSG_MAP(CMyDialog)
        // Карта сообщений
    END_MSG_MAP()
};

Обратите внимание, что константа IDD описывается в секции public. Если описать её в private-секции, функция базового класса CDialogImpl<>::DoModal не сможет к ней обратиться, что приведёт к ошибке.

Далее полученный класс можно использовать для создания как модальных, так и немодальных диалогов, например:

// Создаём модальный диалог
CMyDialog modal;
modal.DoModal();

// Создаём немодальный диалог
CMyDialog modeless;
modeless.Create(HWND_DESKTOP);
Класс CAxDialogImpl<>

Класс CAxDialogImpl<> очень похож на предыдущий. Вся разница в том, что вместо функции DialogBoxParam он использует функцию AtlAxDialogBox, а вместо функции CreateDialogParam - функцию AtlAxCreateDialog:

    // modal dialogs
    int DoModal(HWND hWndParent = ::GetActiveWindow(), LPARAM dwInitParam = NULL)
    {
        ATLASSERT(m_hWnd == NULL);
        _Module.AddCreateWndData(&m_thunk.cd, (CDialogImplBaseT< TBase >*)this);

#ifdef _DEBUG
        m_bModal = true;
#endif //_DEBUG

        return AtlAxDialogBox(_Module.GetResourceInstance(), MAKEINTRESOURCE(T::IDD),
                    hWndParent, (DLGPROC)T::StartDialogProc, dwInitParam);
    }

    ...

    // modeless dialogs
    HWND Create(HWND hWndParent, LPARAM dwInitParam = NULL)
    {
        ATLASSERT(m_hWnd == NULL);
        _Module.AddCreateWndData(&m_thunk.cd, (CDialogImplBaseT< TBase >*)this);

#ifdef _DEBUG
        m_bModal = false;
#endif //_DEBUG

        HWND hWnd = AtlAxCreateDialog(_Module.GetResourceInstance(), MAKEINTRESOURCE(T::IDD),
                    hWndParent, (DLGPROC)T::StartDialogProc, dwInitParam);
        ATLASSERT(m_hWnd == hWnd);
        return hWnd;
    }

Эти функции, в отличие от своих аналогов из Win32 API, могут создавать диалоги, содержащие ActiveX-контролы. Мы не будем рассматривать их реализацию, поскольку тема использования ActiveX-контролов выходит за рамки данной статьи.

Класс CSimpleDialog<>

Чтобы создавать диалоги на базе класса CDialogImpl<>, необходимо каждый раз порождать от него собственные классы. Это довольно утомительно. Класс CSimpleDialog<> предназначен для отображения простейших модальных диалогов, содержащих только статическую информацию и стандартные кнопки, такие как "OK" и "Отмена". Кроме функции DoModal, которая реализована почти так же, как в классе CDialogImpl<>, этот класс предоставляет собственную карту сообщений и обработчики OnInitDialog и OnCloseCmd. Последний вызывается в ответ на нажатие любой кнопки со стандартным идентификатором (IDOK, IDCANCEL, IDABORT, IDRETRY, IDIGNORE, IDYES или IDNO) и закрывает диалог.

Обратите внимание, что идентификатор ресурса диалога в классе CSimpleDialog<> задаётся не как константа, а как первый параметр шаблона. Благодаря этому класс можно использовать, не порождая от него собственных классов. Если, к примеру, вы нарисовали в редакторе диалоговое окно About и назначили ему идентификатор IDD_ABOUT, отобразить его можно, используя класс CSimpleDialog<> напрямую:

CSimpleDialog<IDD_ABOUT> dlg;
dlg.DoModal();

Ещё раз подчеркну, что класс CSimpleDialog<> не содержит реализации метода Create, а поэтому не позволяет создавать немодальные диалоги. Методы EndDialog и DestroyWindow также отсутствуют.

Класс CWinDataExchange<>: механизм DDX в стиле WTL

Механизм динамического обмена данными (DDX - Dynamic Data eXchange) используется для обмена данными между контролами и переменными вашей программы. Термин DDX был введён в MFC, хотя сам механизм под разными названиями существует и в других библиотеках. В WTL он также присутствует. Его реализация содержится в классе CWinDataExchange<>.

Прежде чем рассказывать про класс CWinDataExchange<>, скажу несколько слов об общих принципах реализации дополнительной функциональности в WTL.

Обычно дополнительные возможности WTL реализуются в отдельных классах. Чтобы получить доступ к этим возможностям, необходимо произвести свой класс от всех классов WTL, содержащих нужную нам функциональность. Далее каждый из базовых классов конфигурируется с помощью соответствующей карты (map), которая составляется из специально предусмотренных для этой цели макросов. Обычно карта начинается макросом BEGIN_XXX_MAP и заканчивается макросом END_XXX_MAP (XXX обозначает некоторый идентификатор, разъясняющий назначение карты). Между ними располагаются все остальные макросы карты.

Некоторые механизмы WTL, подключённые к нашему классу, требуют также начальной инициализации, которую можно выполнить, например, в обработчике сообщения WM_INITDIALOG.

Настроив нужные нам механизмы WTL, мы можем использовать их, вызывая или переопределяя предусмотренные для этой цели методы.

Вернёмся к механизму DDX. Чтобы использовать его, включите в список базовых классов вашего диалога (или другого окна, содержащего контролы) класс CWinDataExchange<> (описан в файле atlddx.h). В качестве параметра шаблона задаётся имя вашего производного класса. Например:

class CMyDialog : public CDialogImpl<CMyDialog>, public CWinDataExchange<CMyDialog>
{
    ...
};

Следующий шаг - включить в public-секцию вашего класса карту DDX. Каждая строчка в этой карте связывает идентификатор контрола с некоторой переменной в вашей программе. Обычно это переменная-член класса, но она может быть и глобальной/статической. В обмене могут участвовать числовые или текстовые данные с ограничениями или без них. Список макросов, из которых строится карта DDX, приведён в таблице 1.

Макрос | Описание
BEGIN_DDX_MAP(thisClass) | Начало карты DDX. thisClass - имя класса, в котором содержится карта.
DDX_TEXT(nID, var) | Связывает строковую переменную var с контролом nID (здесь и далее nID - это идентификатор контрола). Переменная var может иметь тип TCHAR*, BSTR, CComBSTR или CString. Обмен данными осуществляется при помощи функций SetWindowText и GetWindowText. Чаще всего макрос используется для статических контролов и полей ввода, хотя может применяться и с другими окнами.
DDX_TEXT_LEN(nID, var, len) | Аналогичен предыдущему, но длина строки ограничивается значением len. Попытка передать строку, длина которой превышает len, приведёт к ошибке валидации (об ошибках немного позже).
DDX_INT(nID, var) | Связывает целочисленную переменную var с контролом nID.
DDX_INT_RANGE(nID, var, min, max) | Аналогичен предыдущему, но передаваемое значение должно лежать в диапазоне от min до max. Невыполнение этого условия приведёт к ошибке валидации.
DDX_UINT(nID, var) | Связывает целочисленную беззнаковую переменную var с контролом nID.
DDX_UINT_RANGE(nID, var, min, max) | Аналогичен предыдущему, но передаваемое значение должно лежать в диапазоне от min до max. Невыполнение этого условия приведёт к ошибке валидации.
DDX_FLOAT(nID, var) | Связывает переменную с плавающей точкой var с контролом nID. var может иметь тип float или double. Макрос DDX_FLOAT будет доступен, только если вы определите макрос _ATL_USE_DDX_FLOAT перед включением заголовочного файла atlddx.h.
DDX_FLOAT_RANGE(nID, var, min, max) | Аналогичен предыдущему, но передаваемое значение должно лежать в диапазоне от min до max. Невыполнение этого условия приведёт к ошибке валидации.
DDX_CONTROL(nID, obj) | Связывает объект obj с контролом nID. Для связывания используется метод obj.SubclassWindow, поэтому объект должен принадлежать классу CWindowImplBaseT<> или производному от него.
DDX_CHECK(nID, var) | Привязывает переменную var типа int к флагу checked кнопки nID. Для обмена данными используются сообщения BM_SETCHECK и BM_GETCHECK.
DDX_RADIO(nID, var) | Связывает переменную var типа int с группой переключателей. Контрол nID должен быть первым в группе.
END_DDX_MAP() | Этот макрос завершает карту DDX. Не имеет параметров.

Рассмотрим пример карты DDX для диалога, который позволяет вводить имя, адрес и номер телефона.

    BEGIN_DDX_MAP(CMyDialog)
        DDX_TEXT_LEN(IDC_NAME, m_name, 20)
        DDX_TEXT(IDC_ADDRESS, m_address)
        DDX_UINT_RANGE(IDC_PHONE, m_phone, 0, 9999999)
    END_DDX_MAP()
ПРИМЕЧАНИЕ
Если вы использовали MFC, вы, вероятно, заметили сходство карты DDX с виртуальной функцией CWnd::DoDataExchange. Они действительно похожи. Одно отличие состоит в том, что макросы WTL не используют вспомогательную структуру, аналогичную CDataExchange из MFC. Это несколько упрощает их использование. Второе отличие - в WTL собственно обмен и валидация объединены вместе, а в MFC им соответствуют различные функции (DDX_* для обмена данными и DDV_* для валидации).

Вот и всё. Никакой дополнительной инициализации механизм DDX в WTL не требует. Чтобы выполнить обмен данными, используйте функцию DoDataExchange. Вот её прототип:

BOOL DoDataExchange(BOOL bSaveAndValidate = FALSE, UINT nCtlID = (UINT)-1)

Параметр bSaveAndValidate задаёт направление обмена (FALSE или DDX_LOAD соответствует записи значений из переменных в контролы, а TRUE или DDX_SAVE - из контролов в переменные). Второй параметр задаёт идентификатор контрола, с которым необходимо произвести обмен. Значение по умолчанию (-1) соответствует всем контролам, упомянутым в карте DDX. Функция DoDataExchange возвращает TRUE, если обмен данными был успешным, или FALSE в противном случае.

ПРИМЕЧАНИЕ
В MFC обмен данными осуществляет функция CWnd::UpdateData, похожая на DoDataExchange из WTL. Отличие в том, что функция UpdateData не позволяет задавать идентификатор контрола. Вместо этого она всегда воздействует на все контролы, прописанные в функции CWnd::DoDataExchange. Реализация в WTL несколько гибче, но было бы ещё лучше, если бы разработчики WTL предусмотрели разбиение карты DDX на подкарты (как это сделано для карт сообщений). Часто в реальной программе требуется выполнить обмен данными не с одним контролом и не со всеми контролами, а с некоторым их подмножеством.

Иногда в процессе обмена данными возникают ошибки. Их делят на две разновидности: ошибки обмена (data exchange errors) и ошибки валидации (data validation errors). Ошибки обмена возникают, когда контрол не содержит значения, соответствующего типу связанной с ним переменной (например, поле ввода, связанное с переменной типа int, содержит пробелы или другие нецифровые символы). Ошибки валидации фиксируются в случае несоответствия передаваемого значения и наложенных на него ограничений (максимальная длина строки, минимальное и максимальное значение числа). В случае возникновения ошибки обмена вызывается виртуальная функция OnDataExchangeError, а при возникновении ошибки валидации - виртуальная функция OnDataValidateError. Дальнейший процесс обмена данными прерывается, а DoDataExchange возвращает FALSE, сигнализируя о неуспехе операции.

Класс CWinDataExchange<> предоставляет свои реализации функций OnDataExchangeError и OnDataValidateError. Они обе совершенно одинаковы.

// Overrideables
    void OnDataExchangeError(UINT nCtrlID, BOOL /*bSave*/)
    {
        // Override to display an error message
        ::MessageBeep((UINT)-1);
        T* pT = static_cast<T*>(this);
        ::SetFocus(pT->GetDlgItem(nCtrlID));
    }

    void OnDataValidateError(UINT nCtrlID, BOOL /*bSave*/, _XData& /*data*/)
    {
        // Override to display an error message
        ::MessageBeep((UINT)-1);
        T* pT = static_cast<T*>(this);
        ::SetFocus(pT->GetDlgItem(nCtrlID));
    }

Как видим, эти функции издают звуковой сигнал и устанавливают фокус ввода на контрол, в котором содержится неверное значение. Вы можете изменить это поведение на любое другое. Обратите внимание на структуру _XData, которая передаётся в функцию OnDataValidateError. Она содержит информацию об ограничении, которое было нарушено. Вот как описана эта структура в файле atlddx.h.

// Helpers for validation error reporting
    enum _XDataType
    {
        ddxDataNull = 0,
        ddxDataText = 1,
        ddxDataInt = 2,
        ddxDataFloat = 3,
        ddxDataDouble = 4
    };

    struct _XTextData
    {
        int nLength;
        int nMaxLength;
    };

    struct _XIntData
    {
        long nVal;
        long nMin;
        long nMax;
    };

    struct _XFloatData
    {
        double nVal;
        double nMin;
        double nMax;
    };

    struct _XData
    {
        _XDataType nDataType;
        union
        {
            _XTextData textData;
            _XIntData intData;
            _XFloatData floatData;
        };
    };

Соответственно, в функции OnDataValidateError нужно проанализировать значение поля nDataType и выбрать в зависимости от него структуру textData, intData или floatData, которая и будет содержать информацию о нарушенном ограничении.

ПРИМЕЧАНИЕ
MFC не позволяет повлиять на отображение ошибки валидации. Если вы используете функции DDV_*, вы всегда будете получать сообщение об ошибке валидации в виде message box'а. Изменить это поведение нельзя, можно только отказаться от DDV_* и использовать для валидации функции "собственного изготовления".
Как это всё работает

Теперь посмотрим, как механизм DDX выглядит "изнутри". К счастью, в его реализации нет ничего сложного. От класса CWinDataExchange<> ваш класс наследует функции DDX_Text, DDX_Int, DDX_Float, DDX_Control, DDX_Check и DDX_Radio, которые и выполняют собственно обмен данными. Некоторые из них перегружены, а DDX_Int и вовсе оформлена как шаблон, что позволяет работать с самыми разными целыми типами.

После обработки препроцессором карта DDX превращается в функцию DoDataExchange. Макросы BEGIN_DDX_MAP и END_DDX_MAP создают пролог и эпилог этой функции. "Заготовка" карты:

    BEGIN_DDX_MAP(CMyDialog)
        // Другие макросы карты DDX
    END_DDX_MAP()

превращается в:

    BOOL DoDataExchange(BOOL bSaveAndValidate = FALSE, UINT nCtlID = (UINT)-1)
    {
        bSaveAndValidate;
        nCtlID;

        // Другие макросы карты DDX

        return TRUE;
    }

Что касается остальных макросов DDX_*, то все они реализованы примерно одинаково. Сначала они сравнивают свой идентификатор контрола nID с идентификатором nCtlID, который был передан в функцию DoDataExchange. Если идентификаторы равны или nCtlID равен -1, макрос вызывает соответствующую функцию DDX_*. Далее проверяется возвращаемое значение, и если оно равно FALSE, обмен данными прекращается. Рассмотрим для примера макросы DDX_TEXT и DDX_TEXT_LEN. Обратите внимание, что они используют одну и ту же функцию DDX_Text, но передают ей разные параметры.

#define DDX_TEXT(nID, var) \
        if(nCtlID == (UINT)-1 || nCtlID == nID) \
        { \
            if(!DDX_Text(nID, var, sizeof(var), bSaveAndValidate)) \
                return FALSE; \
        }

#define DDX_TEXT_LEN(nID, var, len) \
        if(nCtlID == (UINT)-1 || nCtlID == nID) \
        { \
            if(!DDX_Text(nID, var, sizeof(var), bSaveAndValidate, TRUE, len)) \
                return FALSE; \
        }

Теперь мы знаем, как устроены карты DDX. Это может помочь нам писать их более эффективно. Например, мы можем написать в карте DDX следующее:

    BEGIN_DDX_MAP(CMyDialog)
        ...
        for(int i=0; i<100; i++)
            DDX_INT(IDC_BASE+i, m_numbers[i]);
        ...
    END_DDX_MAP()

Это гораздо удобнее, чем вставлять в карту 100 записей.

Использование DDX_TEXT

Если с макросами DDX_INT, DDX_UINT и DDX_FLOAT проблем обычно не возникает, то макрос DDX_TEXT может стать источником неприятностей. Чтобы с ними разобраться, рассмотрим реализацию функции DDX_Text.

    BOOL DDX_Text(UINT nID, LPTSTR lpstrText, int nSize, BOOL bSave, BOOL bValidate = FALSE, int nLength = 0)
    {
        T* pT = static_cast<T*>(this);
        BOOL bSuccess = TRUE;

        if(bSave)
        {
            HWND hWndCtrl = pT->GetDlgItem(nID);
            int nRetLen = ::GetWindowText(hWndCtrl, lpstrText, nSize);
            if(nRetLen < ::GetWindowTextLength(hWndCtrl))
                bSuccess = FALSE;
        }

        ...

        return bSuccess;
    }

Как видим, размер буфера задаётся параметром nSize. Но рассчитывается этот размер по меньшей мере странно:

#define DDX_TEXT(nID, var) \
        if(nCtlID == (UINT)-1 || nCtlID == nID) \
        { \
            if(!DDX_Text(nID, var, sizeof(var), bSaveAndValidate)) \

                return FALSE; \
        }

#define DDX_TEXT_LEN(nID, var, len) \
        if(nCtlID == (UINT)-1 || nCtlID == nID) \
        { \
            if(!DDX_Text(nID, var, sizeof(var), bSaveAndValidate, TRUE, len)) \
                return FALSE; \
        }

Другими словами, за размер буфера принимается размер переменной var, которая связывается с контролом. Отсюда следует два вывода. Во-первых, переменная var может быть только статическим массивом, а динамическим - нет. Во-вторых, в программе, использующей набор символов Unicode, этот размер будет всегда определяться неправильно. Выход в том и в другом случае - отказаться от макроса DDX_TEXT и обратиться к функции DDX_Text напрямую, передав ей правильный размер. Замечу также, что при передаче строки из переменной в контрол размер буфера значения не имеет, так что если вы передаёте данные только в этом направлении, DDX_TEXT использовать можно.

С набором символов Unicode связана ещё одна интересная проблема. Посмотрим на следующую карту DDX:

    LPTSTR m_msg;

    BEGIN_DDX_MAP(CMyDialog)
        ...
        DDX_Text(IDC_MESSAGE, m_msg, ...)
        ...
    END_DDX_MAP()

Если попытаться откомпилировать этот код, задав макрос UNICODE, компилятор выдаст следующую ошибку: 'DDX_Text' : ambiguous call to overloaded function (неоднозначность при обращении к перегруженной функции). Дело в том, что в классе CWinDataExchange<> существует несколько перегруженных версий DDX_Text. Вот две из них:

// Text exchange
    BOOL DDX_Text(UINT nID, LPTSTR lpstrText, int nSize, BOOL bSave, BOOL bValidate = FALSE, int nLength = 0)
    {
        ...
    }

    BOOL DDX_Text(UINT nID, BSTR& bstrText, int /*nSize*/, BOOL bSave, BOOL bValidate = FALSE, int nLength = 0)
    {
        ...
    }

Если макрос UNICODE определён, LPTSTR превращается в wchar_t*, а BSTR& - в wchar_t*&. Получается неоднозначность. Чтобы решить эту проблему, можно переписать карту DDX так:

    BEGIN_DDX_MAP(CMyDialog)
        ...
        DDX_Text(IDC_MESSAGE, (TCHAR * const)m_msg, ...)
        ...
    END_DDX_MAP()

Поскольку в C++ константный указатель можно передать по значению, но не по ссылке, неоднозначность тем самым удаётся разрешить. В любом случае, если вы собираетесь компилировать программу с поддержкой Unicode, я советую вам использовать для обмена текстом переменные типа CString. Это избавит вас от многих проблем, подобных рассмотренным выше.

Использование DDX_CONTROL

Макрос DDX_CONTROL связывает контрол с объектом класса, порождённого от CWindowImplBaseT<>. Если вы знакомы с MFC, вы знаете, что там обычной практикой является связывание объекта класса CWnd (или его потомка) с контролом, даже если вам не нужно подключать его к карте сообщений, а просто вызвать несколько обёрток типа CWnd::GetWindowText или CListCtrl::GetItem. Это создаёт значительный, причём совершенно ненужный, перерасход ресурсов. Не используйте макрос DDX_CONTROL из WTL подобным образом. Он используется, если вам действительно необходимо заменить оконную процедуру контрола и обрабатывать его сообщения через карту сообщений.

Если же вам нужно просто использовать функции-обёртки из класса CWindow для работы с контролом, достаточно получить хэндл этого контрола с помощью GetDlgItem, а затем присвоить его объекту класса. Удобно делать это в обработчике WM_INITDIALOG. Например:

class CMyDialog : public CDialogImpl<CMyDialog>,
                  public CWinDataExchange<CMyDialog>,
{
private:
    CWindow m_control;

    ...

public:
    BEGIN_MSG_MAP_EX(CMyDialog)
        MSG_WM_INITDIALOG(OnInitDialog)
        ...
    END_MSG_MAP()

    LRESULT OnInitDialog(HWND, LPARAM)
    {
        m_control = GetDlgItem(IDC_SOME_CONTROL);
        ...
    }

    ...
};

Ниже в этой статье мы увидим, что кроме CWindow в WTL существует целый набор классов для работы с контролами - CStatic, CButton, CEdit и т. д. Их можно использовать так же, как и CWindow в приведённом выше примере.

Использование DDX_RADIO

Макрос DDX_RADIO используется для работы сразу с целой группой переключателей. При этом переменная var, связанная с группой, содержит порядковый номер выбранного переключателя в группе (нумерация начинается с нуля). Значение -1 соответствует состоянию группы, в котором ни один из переключателей не выбран.

А что, если нам нужно связать переменную не со всей группой, а с конкретным переключателем из неё? В этом случае нужно просто воспользоваться макросом DDX_CHECK вместо DDX_RADIO.

Класс CUpdateUI<>: обновление дочерних окон в стиле WTL

Вероятно, вы не раз видели диалоги, в которых манипуляции с одним контролом приводят к изменению некоторых других (они включаются/отключается, текст на них меняется и т. д.). В WTL, как и в MFC, существует специальный механизм, поддерживающий изменение состояния контролов в диалоге (или в любом другом окне). На самом деле, этот механизм универсален и применяется также для обновления состояния пунктов меню, кнопок на панели инструментов и т. д.

Чтобы подключить механизм обновления дочерних контролов к вашему диалогу, добавьте в список базовых классов класс CUpdateUI<>, который описан в файле atlframe.h. Кроме этого, необходимо написать карту обновления пользовательского интерфейса (далее карта UI). Набор макросов, из которых составляется карта UI, минимален. Их всего 3 штуки. Все они описаны в таблице 2.

Макрос | Описание
BEGIN_UPDATE_UI_MAP(thisClass) | Начало карты UI. thisClass - имя класса, в котором содержится карта.
UPDATE_ELEMENT(nID, wType) | Определяет, какие типы элементов пользовательского интерфейса с идентификатором nID должны обновляться. Нужные типы объединяются с помощью операции "ИЛИ" и передаются в качестве второго параметра макроса wType. WTL распознаёт следующие типы: UPDUI_MENUPOPUP (пункт всплывающего меню), UPDUI_MENUBAR (пункт полоски меню), UPDUI_CHILDWINDOW (дочернее окно, контрол), UPDUI_TOOLBAR (кнопка на панели инструментов) и UPDUI_STATUSBAR (панель на строке состояния). В этой статье мы сосредоточимся на контролах, а об остальных элементах поговорим, когда доберёмся до окон-рамок.
END_UPDATE_UI_MAP() | Этот макрос завершает карту UI. Не имеет параметров.

После того, как карта UI добавлена в класс, остаётся один завершающий штрих. Вы должны зарегистрировать все контейнеры элементов пользовательского интерфейса, которые нужно обновлять. В случае с контролами в качестве контейнера выступает сам диалог. В случае с меню это окно, содержащее меню. И так далее. Для каждого контейнера существует своя функция регистрации: UIAddMenuBar для меню, UIAddToolBar для панелей иснтрументов, UIAddStatusBar для строк состояния и UIAddChildWindowContainer для контейнеров дочерних окон. Все перечисленные функции принимают хэндл окна-контейнера и позвращают BOOL, сигнализирующий об успехе или неуспехе регистрации. В случае с диалогом регистрировать контейнер удобно в обработчике сообщения WM_INITDIALOG.

Итак, инициализация закончена. Теперь мы можем изменять состояния контролов, идентификаторы которых включены в карту UI. Для этого используется ещё один набор функций с префиксом UI. Все они перечислены в таблице 3.

Функция | Описание
BOOL UIEnable(int nID, BOOL bEnable, BOOL bForceUpdate = FALSE) | Изменяет состояние доступности элементов с идентификатором nID в соответствии со значением bEnable. Флаг принудительного обновления bForceUpdate задаётся, когда нужно фактически обновить элемент, даже если его текущее состояние соответствует желаемому и менять ничего не надо. Поскольку все функции используют этот флаг одинаково, я больше не буду на нём останавливаться.
BOOL UISetCheck(int nID, int nCheck, BOOL bForceUpdate = FALSE) | Изменяет состояние флага "checked" элементов с идентификатором nID в соответствии со значением nCheck. Из контролов эту функцию имеет смысл применять только к кнопкам, так как другие контролы не имеют такого флага. nCheck может принимать одно из трёх значений: 0, если кнопка "отжата", 1, если нажата и 2, если она находится в "третьем состоянии" (кнопка активна, но отрисовывается серым цветом). Третье состояние имеется только у конпок со стилями BS_3STATE или BS_AUTO3STATE.
BOOL UISetRadio(int nID, BOOL bRadio, BOOL bForceUpdate = FALSE) | Изменяет состояние флага "radio" элементов с идентификатором nID в соответствии со значением nRadio. Для контролов эта функция работает аналогично предыдущей.
BOOL UISetText(int nID, LPCTSTR lpstrText, BOOL bForceUpdate = FALSE) | Изменяет текст элементов с идентификатором nID на заданный в параметре lpstrText.
BOOL UISetState(int nID, DWORD dwState) | Эта функция позволяет изменить сразу несколько флагов, связанных с элементами nID. Эти флаги объединяются операцией "ИЛИ" и передаются в качестве параметра dwState. Можно использовать флаги UPDUI_DISABLED, UPDUI_CHECKED, UPDUI_CHECKED2 (этот флаг соответствует "третьему состоянию" кнопки), UPDUI_RADIO и UPDUI_DEFAULT. Замечу, что флаг UPDUI_DEFAULT можно менять, только используя функцию UISetState. Специальной функции для его изменения нет. Этот флаг позволяет сделать элемент используемым по умолчанию. Обратите внимание, что функция UISetState не использует флаг bForceUpdate и всегда обновляет элемент, вне зависимости от его текущего состояния.
DWORD UIGetState(int nID) | Функция, обратная предыдущей. Возвращает набор флагов, характеризующих состояние элемента.

Функции, которые мы только что рассмотрели, не изменяют фактическое состояние элементов. Они только записывают новые значения во внутренние структуры класса CUpdateUI<>. Чтобы внесённые изменения вступили в силу, нужно вызвать специальную функцию. Для каждого типа контейнеров существует своя функция: UIUpdateMenuBar для меню, UIUpdateToolBar для панели инструментов, UIUpdateStatusBar для строки состояния и UIUpdateChildWindows для контейнера дочерних окон. Каждая из этих функций принимает флаг bForceUpdate. Используйте его, чтобы принудительно обновить все элементы, прописанные в карте UI.

Как это всё работает

Посмотрим, как устроен класс CUpdateUI<>. Карта UI, которую вы создаёте, превращается в массив структур _AtlUpdateUIMap.

    struct _AtlUpdateUIMap
    {
        WORD m_nID;
        WORD m_wType;
    };

Каждая структура содержит в точности те значения, которые вы передаёте макросу UPDATE_ELEMENT в качестве параметров. Массив завершается структурой со значениями {(WORD)-1, 0}. Для обращения к нему используется функция GetUpdateUIMap, внутри которой он описывается как статическая переменная. Этот массив один на все объекты класса, порождённого от CUpdateUI<>. Кроме этого, каждый объект класса наследует от CUpdateUI<> переменные m_UIElements, m_pUIData и m_wDirtyType.

m_UIElements - это массив контейнеров, для редактирования которого и используется семейство функций UIAddXXX. Кстати, странно, что разработчики WTL не предусмотрели средства для удаления контейнеров из этого массива. Но тут уже ничего не поделаешь.

m_pUIData - массив структур _AtlUpdateData. Количество элементов в этом массиве в точности соответствует количеству записей в карте UI. Каждая структура _AtlUpdateData содержит флаги состояния (те самые, которые меняет функция UISetState) и указатель на строку, которые должны быть назначены элементу. Место для строк распределяется динамически. Вот как описана структура _AtlUpdateUIData.

    struct _AtlUpdateUIData
    {
        WORD m_wState;
        void* m_lpData;
    };

Теперь понятно, что делают функции типа UIEnable и UISetCheck. Они просто изменяют поля структуры _AtlUpdateUIData, соответствующей заданному элементу. Что касается семейства функций UIUpdateXXX, то они используют данные из m_pUIData, чтобы обновить элементы управления.

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

Где обновлять элементы

Механизм обновления элементов пользовательского интерфейса, реализованный в WTL, не навязывает вам определённой стратегии обновления, а просто избавляет вас от рутинной работы. Вы можете обновлять элементы всякий раз, когда пользователь делает какое-то действие. В этом случае по всей программе будут разбросаны "пачки" вызовов функций обновления UIEnable, UISetText и т. д. Но совершенно очевидно, что такой подход раздувает и запутывает ваш код. Гораздо лучше написать одну функцию, которая обновляет все элементы в зависимости от текущего состояния программы. Потом к этой функции можно обращаться всякий раз, когда состояние элементов может измениться.

Альтернативный вариант, который, кстати, используется в MFC, заключается в обновлении элементов в фоне, то есть когда очередь сообщений пуста. Если вы используете немодальный диалог, вам будет нетрудно реализовать эту идею и в WTL: для этого достаточно зарегистрировать объект диалога в цикле сообщений как фоновый обработчик, а затем обновлять элементы в функции OnIdle. Однако если диалог модальный, цикл сообщений скрыт внутри функции DialogBoxParam и фоновая обработка в стиле WTL недоступна. В этом случае можно использовать сообщение WM_ENTERIDLE (модальный диалог посылает его родительскому окну, когда очередь сообщений исчерпана) или вообще отказаться от фоновой обработки.

[ ПРОДОЛЖЕНИЕ СЛЕДУЕТ ]

Это все на сегодня. Пока!

Алекс Jenter jenter@rsdn.ru
Duisburg, 2001. Публикуемые в рассылке материалы принадлежат сайту RSDN.


ПРОГРАММИРОВАНИЕ НА V I S U A L C + +
РАССЫЛКА САЙТА
RSDN.RU
Выпуск No. 55 от 18 ноября 2001 г.

РАССЫЛКА ЯВЛЯЕТСЯ ЧАСТЬЮ ПРОЕКТА RSDN , НА САЙТЕ КОТОРОГО ВСЕГДА МОЖНО НАЙТИ ВСЮ НЕОБХОДИМУЮ РАЗРАБОТЧИКУ ИНФОРМАЦИЮ, СТАТЬИ, ФОРУМЫ, РЕСУРСЫ, ПОЛНЫЙ АРХИВ ПРЕДЫДУЩИХ ВЫПУСКОВ РАССЫЛКИ И МНОГОЕ ДРУГОЕ.

Добрый день!

Сегодня в выпуске - продолжение второй части статьи "Использование WTL".

Если вы еще не читали первую часть, ее можно найти на RSDN.

CТАТЬЯ

Использование WTL
Часть 2. Диалоги и контролы (продолжение)
Автор: Александр Шаргин
Класс CDialogResize<>: масштабирование диалогов в стиле WTL

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

Обычно эта проблема решается так. Диалогу назначается стиль WS_THICKFRAME (Border: resizing в редакторе ресурсов). Затем в программе перехватывается сообщение WM_SIZE, сигнализирующее об изменении размеров диалога. В ответ на него программа соответствующим образом изменяет размеры контролов в диалоге. Этот подход универсален и достаточно прост в реализации, но требует написания большого количества кода, связанного с пересчётом координат. Поэтому в WTL введён класс, который в ряде случаев избавит вас от рутинной работы по масштабированию контролов. Этот класс называется CDialogResize<>. Он описан в файле atlframe.h. Хотя этот класс не является универсальным, он подойдёт в большинстве случаев. Замечу, что его можно применять с любым окном, содержащим дочерние окна, но чаще всего он применяется именно с диалогами.

Итак, чтобы воспользоваться поддержкой масштабирования, которую предоставляет WTL, нужно включить в число базовых классов вашего диалога класс CDialogResize<>, задав в качестве параметра шаблона имя порождаемого класса. После этого вам, как обычно, потребуется написать карту - на этот раз карту масштабирования. Макросы, из которых она формируется, приведены в таблице 4.

Макрос | Описание
BEGIN_DLGRESIZE_MAP(thisClass) | Начало карты масштабирования. thisClass - имя класса, в котором содержится карта.
END_DLGRESIZE_MAP() | Этот макрос завершает карту масштабирования.
DLGRESIZE_CONTROL(id, flags) | Этот макрос определяет, каким образом должен масштабироваться контрол с идентификатором id. Для этого в WTL определено несколько флагов, которые нужно объединить операцией логического "ИЛИ" и передать в качестве второго параметра макроса flags. Вы можете использовать флаги DLSZ_MOVE_X и DLSZ_MOVE_Y (перемещение вдоль осей X и Y соответственно), DLSZ_SIZE_X и DLSZ_SIZE_Y (изменение ширины и высоты контрола), а также флаг DLSZ_REPAINT, если после масштабирования контрола его нужно перерисовывать (то есть вызывать для него функцию Invalidate).
BEGIN_DLGRESIZE_GROUP() | Контролы, включённые в карту масштабирования, можно группировать. Об эффектах группировки мы поговорим позже. Макрос BEGIN_DLGRESIZE_GROUP начинает группу контролов. Группы не могут быть вложенными.
END_DLGRESIZE_GROUP() | Завершает группу контролов. Каждому макросу BEGIN_DLGRESIZE_GROUP должен соответствовать ровно один макрос END_DLGRESIZE_GROUP.

Кроме написания карты масштабирования, необходимо выполнить ещё два действия. Во-первых, класс CDialogResize<> имеет свою собственную карту сообщений. В частности, она содержит обработчик сообщения WM_SIZE, который инициирует перемасштабирование контролов при каждом изменении размеров диалога. Эту карту сообщений следует подключить к карте сообщений вашего диалога, используя макрос CHAIN_MSG_MAP:

    BEGIN_MSG_MAP(CMyDialog)
        ...
        CHAIN_MSG_MAP(CDialogResize<CMyDialog>)
        ...
    END_MSG_MAP()

Во вторых, после того, как ваш дилог создан, необходимо инициализировать внутренние структуры WTL, связанные с масштабированием. Это делается при помощи функции DlgResize_Init. Удобно вызывать её из обработчика сообщения WM_INITDIALOG. Функция DlgResize_Init имеет следующий прототип:

void DlgResize_Init(bool bAddGripper = true,
                    bool bUseMinTrackSize = true,
                    DWORD dwForceStyle = WS_THICKFRAME | WS_CLIPCHILDREN)

В большинстве случаев на параметры можно не обращать внимание, так как значения по умолчанию вполне удовлетворяют всем нуждам. Параметр bAddGripper указывает, нужно ли добавить к диалогу "гриппер" - маленький уголок, за который можно ухватиться курсором и изменить размеры диалога. Флаг bUseMinTrackSize определяет, нужно ли ограничивать минимальные размеры диалога. В большинстве случаев это хорошая идея, так как сильно уменьшенный дилог всё равно плохо выглядит и не удобен для работы с ним. Минимальный размер диалога хранится в переменной m_ptMinTrackSize, которую ваш класс диалога наследует от класса CDialogResize<>. По умолчанию в неё записывается первоначальный размер диалога (тот, который установлен в момент вызова функции DlgResize_Init). Вы можете записать туда любое другое значение. Что касается параметра dwForceStyle, то это просто стиль, который принудительно назначается диалогу в функции DlgResize_Init.

Ещё одна функция из класса CDialogResize<>, о которой следует упомянуть, - DlgResize_UpdateLayout. Эта функция принудительно пересчитывает координаты всех контролов в зависимости от переданных ей размеров диалога (cx и cy). Именно она вызывается из обработчика сообщения WM_SIZE, но при необходимости вы можете вызывать её в любом другом месте.

Как составлять карту масштабирования

На самом деле, единственная проблема с классом CDialogResize<> состоит в том, чтобы правильно составить карту масштабирования. Для этого нужно чётко понимать, как работают флаги DLSZ_XXX. Эти флаги по-разному действуют на контрол в группе или без неё.

Сначала посмотрим, как флаги DLSZ_XXX действуют на контрол, не включённый в группу. Допустим, размеры диалога изменились на dx и dy соответственно. Тогда:

  • DLSZ_SIZE_X: ширина контрола изменяется на dx.
  • DLSZ_SIZE_Y: высота контрола изменяется на dy.
  • DLSZ_MOVE_X: контрол двигается вдоль оси X на dx. Флаг DLSZ_SIZE_X при этом игнорируется.
  • DLSZ_MOVE_Y: контрол двигается вдоль оси Y на dy. Флаг DLSZ_SIZE_Y при этом игнорируется.

Как видно из этого описания, задавать одновременно флаги DLSZ_MOVE_X и DLSZ_SIZE_X (а также DLSZ_MOVE_Y и DLSZ_SIZE_Y) бессмысленно, так как в этом случае будут учитываться только флаги DLSZ_MOVE_*.

Описанная схема масштабирования довольно примитивна. Так, очевидно, что к двум расположенным рядом контролам нельзя применять флаг DLSZ_SIZE_*, так как они оба увеличат размер и "заедут" друг на друга. И всё-таки во многих случаях такого механизма оказывается достаточно. Для примера рассмотрим типичный диалог выбора файла (рисунок 2).

Рисунок 2. Схема диалога открытия файла

При масштабировании логично изменять размер контролов диалога следующим образом: растягивать IDC_LEFT_PANE на всю высоту диалога, растягивать IDC_COMBO по горизонтали, отодвигая IDC_TOOLBAR до предела вправо, отодвигать IDC_NAME и IDC_FILTER вниз и растягивать по горизонтали, перемещать кнопки IDOK и IDCANCEL в правый нижний угол и занимать списком файлов IDC_FILE_LIST всё оставшееся место. Чтобы воплотить в жизнь эту схему, следует записать карту масштабирования следующим образом:

    BEGIN_DLGRESIZE_MAP(COpenFileDialog)
        DLGRESIZE_CONTROL(IDC_LEFT_PANE, DLSZ_SIZE_Y)
        DLGRESIZE_CONTROL(IDC_COMBO, DLSZ_SIZE_X)
        DLGRESIZE_CONTROL(IDC_TOOLBAR, DLSZ_MOVE_X)
        DLGRESIZE_CONTROL(IDC_FILE_LIST, DLSZ_SIZE_X | DLSZ_SIZE_Y)
        DLGRESIZE_CONTROL(IDC_NAME, DLSZ_MOVE_Y | DLSZ_SIZE_X)
        DLGRESIZE_CONTROL(IDC_FILTER, DLSZ_MOVE_Y | DLSZ_SIZE_X)
        DLGRESIZE_CONTROL(IDOK, DLSZ_MOVE_Y | DLSZ_MOVE_X)
        DLGRESIZE_CONTROL(IDCANCEL, DLSZ_MOVE_Y | DLSZ_MOVE_X)
    END_DLGRESIZE_MAP()

Теперь поговорим о контролах, объединённых в группу.

ПРЕДУПРЕЖДЕНИЕ
Реализация групп в WTL подразумевает, что все контролы в группе должны располагаться рядом друг с другом по горизонтали или по вертикали. Флаги, которые вы задаёте для контролов в группе, должны относиться только к одному направлению (или X, или Y), но не к обоим сразу. Несоблюдение этих условий приведёт к странным эффектам. Кроме того, напомню ещё раз, что группы не могут быть вложенными.

Группы обрабатываются следующим образом. Сначала вычисляются координаты огибающего прямоугольника группы, то есть минимального прямоугольника, содержащего все контролы в ней. Далее размеры этого прямоугольника увеличиваются на dx и dy соответственно (dx и dy имеют то же значение, что и в обсуждении выше). После этого к каждому контролу в группе применяются следующие правила:

  • DLSZ_MOVE_X: контрол сдвигается вдоль оси X пропорционально изменению ширины группы (то есть её огибающего прямоугольника).
  • DLSZ_MOVE_Y: контрол сдвигается вдоль оси Y пропорционально изменению высоты группы.
  • DLSZ_SIZE_X: действует аналогично DLSZ_MOVE_X, но ширина контрола также изменяется пропорционально изменению ширины группы.
  • DLSZ_SIZE_Y: действует аналогично DLSZ_MOVE_Y, но высота контрола также изменяется пропорционально изменению высоты группы.

Проиллюстрирую сказанное простым примером. Допустим, у нас есть диалог с тремя расположенными в ряд кнопками. Если написать для него карту масштабирования вида:

    BEGIN_DLGRESIZE_MAP(CMyDialog)
        BEGIN_DLGRESIZE_GROUP()
            DLGRESIZE_CONTROL(IDC_BUTTON1, DLSZ_SIZE_X)
            DLGRESIZE_CONTROL(IDC_BUTTON2, DLSZ_SIZE_X)
            DLGRESIZE_CONTROL(IDC_BUTTON3, DLSZ_SIZE_X)
        END_DLGRESIZE_GROUP()
    END_DLGRESIZE_MAP()

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

Рисунок 3. Масштабирование с использованием групп

Как это всё работает

Мы не будем надолго задерживаться на внутренней реализации класса CDialogResize<>, так как там нет почти ничего интересного. Когда вы вызываете функцию DlgResize_Init, начальные положения всех контролов в диалоге запоминаются во внутренних структурах WTL. Функция DlgResize_UpdateLayout использует новые размеры диалога и сохранённые ранее координаты контролов, чтобы назначить им новое положение в соответствии с заданными флагами. Что касается карты масштабирования, она просто превращается в статический массив структур _AtlDlgResizeMap, для доступа к которому используется функция GetDlgResizeMap. Структура _AtlDlgResizeMap хранит заданные вами в карте значения:

    struct _AtlDlgResizeMap
    {
        int m_nCtlID;
        DWORD m_dwResizeFlags;
    };

Хочу отметить несколько особенностей реализации класса CDialogResize<>, которые можно использовать в своих целях.

  1. Элемент может встречаться в карте масштабирования более одного раза.
  2. Элемент, не включённый в группу, двигается относительно его текущего положения.
  3. Элемент, включённый в группу, масштабируется с учётом его начального положения (но без учёта его текущей позиции).

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

    BEGIN_DLGRESIZE_MAP(CMyDialog)
        BEGIN_DLGRESIZE_GROUP()
            DLGRESIZE_CONTROL(IDC_BUTTON1, DLSZ_SIZE_X)
            DLGRESIZE_CONTROL(IDC_BUTTON2, DLSZ_SIZE_X)
            DLGRESIZE_CONTROL(IDC_BUTTON3, DLSZ_SIZE_X)
        END_DLGRESIZE_GROUP()

        DLGRESIZE_CONTROL(IDC_BUTTON1, DLSZ_SIZE_Y)
        DLGRESIZE_CONTROL(IDC_BUTTON2, DLSZ_SIZE_Y)
        DLGRESIZE_CONTROL(IDC_BUTTON3, DLSZ_SIZE_Y)
    END_DLGRESIZE_MAP()

Кроме этого, можно включить элемент в несколько групп. Хотя на его местоположение повлияет только последняя группа, этот приём позволит сложным образом влиять на другие контролы. Но не стоит забывать о чувстве меры. Такие приёмы делают вашу программу более запутанной и более медлительной. Нетривиальное масштабирование контролов в диалоге лучше реализовать вручную, а не заниматься неочевидными фокусами с CDialogResize<>.

Контролы

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

Библиотека WTL предоставляет программисту классы для удобной работы со стандартными контролами, а также предоставляет средства для расширения их функциональности. Кроме того, в WTL входит несколько нестандартных контролов (кнопка с картинками, гиперссылка и др.), которые вы также можете использовать в приложениях. Рассмотрим все эти классы более подробно.

Поддержка cтандартных и общих контролов Windows

Мы с вами уже изучили класс CWindow, который предоставляет целый набор обёрток для функций Win32 API, предназначенных для работы с окнами. При работе с контролами этот класс также можно использовать. Но гораздо удобнее использовать специальные классы контролов, которые описаны в файле atlctrls.h. Полный список этих классов приведён в таблице 5.

Контрол | Соответствующий класс
Статический текст или изображение (static control) | CStatic
Кнопка (button) | CButton
Простой список (list box) | CListBox
Комбинированный список (combo box) | CComboBox
Поле ввода (edit box) | CEdit
Полоса прокрутки (scroll bar) | CScrollBar
Список изображений (image list) | CImageList
Расширенный список (list view) | CListViewCtrl
Дерево (tree view) | CTreeViewCtrl, CTreeViewCtrlEx
Заголовок (header) | CHeaderCtrl
Панель инструментов (toolbar) | CToolBarCtrl
Строка состояния (status bar) | CStatusBarCtrl
Окно с закладками (tab control) | CTabCtrl
Всплывающая подсказка (tooltip) | CToolTipCtrl
Ползунок (trackbar) | CTrackBarCtrl
Регулятор (up-down control) | CUpDownCtrl
Индикатор прогресса (progress bar) | CProgressBarCtrl
Горячая клавиша (hot key) | CHotKeyCtrl
Окно с анимацией (animate control) | CAnimateCtrl
Расширенное поле ввода (rich edit) | CRichEditCtrl
Список с возможностью перетаскивания (drag list box) | CDragListBox
Полоска-контейнер (rebar control) | CReBarCtrl
Комбинированный список с картинками (ComboBoxEx control) | CComboBoxEx
Выбор даты/времени (date and time picker) | CDateTimePickerCtrl
Календарь на меcяц (month calendar) | CMonthCalendarCtrl
"Плоская" полоса прокрутки (flat scroll bar) | CFlatScrollBar
IP-адрес (IP address control) | CIPAddressCtrl
Пейджер (pager control) | CPagerCtrl

Каждый из этих классов порождён от CWindow и содержит все его методы. В дополнение каждый класс предоставляет:

  • Метод GetWndClassName. Этот метод позволяет узнать имя класса окна, соответствующего данному контролу.
  • Метод Create. В отличие от аналогичного метода из класса CWindow, он не принимает имя класса, так как оно извлекается при помощи GetWndClassName.
  • Обёртки для стандартных сообщений, которые используются для управления контролом. Например, для статических контролов это сообщения STM_GETICON, STM_GETIMAGE, STM_SETICON и STM_SETIMAGE. Используя обёртки, вы можете не вспоминать, каким образом упаковываются в wParam и lParam параметры этих сообщений.
  • Обёртки для функций Win32 API, манипулирующих контролом. Такие функции существуют лишь для нескольких контролов (таких, как scroll bar).

Обратите внимание, что функциональность всех классов из atlctrls.h регулируется макросами WINVER, _WIN32_IE и _RICHEDIT_VER. Например, функции, специфичные для контролов из Internet Explorer 4.0 и выше, оформлены так:

#if (_WIN32_IE >= 0x0400)
    ...
#endif //(_WIN32_IE >= 0x0400)

Благодаря этому классы контролов из WTL можно использовать при работе с любой версией контролов, получая при этом доступ ко всему набору возможностей используемой версии.

Полное описание всех функций и классов из atlctrls.h выходит за рамки данной статьи.

"Самодельные" контролы

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

Создавать контролы "с нуля" мы уже умеем. Для этого нужно породить новый класс от CWindowImpl<> и написать обработчики нужных сообщений. Чаще других обрабатываются сообщения WM_CREATE и WM_PAINT, а также клавиатурные и мышиные сообщения. Кроме того, нужно предусмотреть средства для взаимодействия программы с вашим контролом. Для этой цели можно ввести нестандартные сообщения, которые будет понимать ваш контрол, или предусмотреть соответствующие методы в вашем классе.

Если вы решили построить свой контрол на базе существующего, вам также следует использовать класс CWindowImpl<>. Нужно только учесть два момента. Во-первых, базовым классом для вашего контрола должен быть не CWindow, а класс контрола, который вы модифицируете. Базовый класс задаётся во втором параметре шаблона CWindowImpl<> (по умолчанию этот параметр равен CWindow). А во-вторых, для обработки сообщений по умолчанию должна использоваться не функция DefWindowProc (как для обычных окон), а оконная функция соответствующего контрола. Чтобы этого добиться, следует использовать макрос DECLARE_WND_SUPERCLASS вместо DECLARE_WND_CLASS. Этот макрос объявлен так.

#define DECLARE_WND_SUPERCLASS(WndClassName, OrigWndClassName) \
static CWndClassInfo& GetWndClassInfo() \
{ \
    static CWndClassInfo wc = \
    { \
        { sizeof(WNDCLASSEX), 0, StartWindowProc, \
          0, 0, NULL, NULL, NULL, NULL, NULL, WndClassName, NULL }, \
        OrigWndClassName, NULL, NULL, TRUE, 0, _T("") \
    }; \
    return wc; \
}

Параметр WndClassName определяет имя класса вашего нового контрола. В качестве второго параметра OrigWndClassName следует указать имя класса контрола, который вы взяли за основу. При регистрации вашего класса WndClassName WTL скопирует для него параметры из класса с именем OrigWndClassName, а также сохранит адрес оконной процедуры, связанной с этим классом, в переменной CWindowImplBaseT<>::m_pfnSuperWindowProc и будет обращаться к ней для обработки сообщений, которые не были обработаны через карту сообщений.

С учётом всего сказанного, типичный класс контрола выглядит так.

class CMyCoolControl : public CWindowImpl<CMyCoolControl, CEdit>
{
public:
    DECLARE_WND_SUPERCLASS(NULL, CEdit::GetWndClassName())

    BEGIN_MSG_MAP(CMyCoolControl)
        // Карта сообщений
    END_MSG_MAP()

    ...
};

В этом примере новый контрол создаётся на базе поля ввода (которому соответствует класс CEdit). Аналогично используется любой другой контрол.

ПРИМЕЧАНИЕ
Мы уже изучили макрос DDX_CONTROL, входящий в набор макросов DDX. Именно его следует использовать, чтобы связать существующий стандартный контрол (например, нарисованный в редакторе ресурсов) с объектом класса и наделить его дополнительными возможностями.

В библиотеку WTL входит несколько "самодельных" контролов, которые реализованы в файле atlctrlx.h. Вы можете вставлять их в свои программы или использовать как демонстрационные примеры по разработке контролов. Вот список классов, которые написали для вас разработчики WTL.

  • CBitmapButton. Кнопка с рисунками.
  • CCheckListViewCtrl. Расширенный список с "галочками".
  • CHyperLink. Гиперссылка.
  • CMultiPaneStatusBarCtrlImpl. Строка состояния с набором панелей.
  • CWaitCursor. Курсор типа "песочные часы". Этот класс, в отличие от всех предыдущих, не имеет отношения к контролам.

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

Класс CBitmapButton

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

Флаг | Описание
BMPBTN_HOVER | Кнопка с этим стилем реагирует на присутствие курсора мыши: если он расположен над кнопкой, она переводится в состояние "в фокусе". Если этот стиль не задан, состояние "в фокусе" присваивается кнопке, на которую установлен клавиатурный фокус ввода. Замечу также, что кнопка со стилем BMPBTN_HOVER не реагирует на клавиатуру, то есть нажать на неё можно только мышью.
BMPBTN_AUTO3D_SINGLE | К кнопке принудительно добавляется трёхмерная рамка толщиной в 1 пиксель. Используя этот стиль, вы можете избавиться от необходимости рисовать трёхмерные рамки на всех изображениях, связанных с состояниями кнопки.
BMPBTN_AUTO3D_DOUBLE | Аналогичен предыдущему, но к кнопке добавляется рамка толщиной в 2 пикселя (такая, как у всех стандартных кнопок Windows).
BMPBTN_AUTOSIZE | Кнопка автоматически масштабируется под размер изображений, которые с ней связаны.
BMPBTN_SHAREIMAGELISTS | Кнопка использует разделяемый список изображений. Это означает, что он не будет уничтожен в деструкторе класса CBitmapButton.
BMPBTN_AUTOFIRE | Этот стиль имеет отношение к клавиатурному интерфейсу кнопки. Если вы нажали на кнопку, используя клавишу Space, и удерживаете эту клавишу, то кнопка со стилем BMPBTN_AUTOFIRE будет через заданные в системе промежутки времени посылать уведомление BN_CLICKED родительскому окну. Если же этот стиль не задать, уведомление отправится ровно 1 раз - в тот момент, когда вы нажали Space.

Стиль кнопки, а также связанный с ней список изображений, задаются в конструкторе класса CBitmapButton, хотя можно установить/изменить их и позже, используя соответствующие методы. Для задания текста всплывающей подсказки также существуют соответствующий метод. Полный список методов класса CBitmapButton приведён в таблице 7.

Метод | Описание
CBitmapButtonImpl(DWORD dwExtendedStyle = BMPBTN_AUTOSIZE, HIMAGELIST hImageList = NULL) | Конструктор. Параметры - набор расширенных стилей и хэндл списка изображений, который следует связать с кнопкой.
~CBitmapButtonImpl() | Деструктор. Напоминаю, что внутри деструктора будет разрушен список изображений, связанный с кнопкой в данный момент. Чтобы этого не произошло, следует назначить кнопке расширенный стиль BMPBTN_SHAREIMAGELISTS.
BOOL SubclassWindow(HWND hWnd) | Метод для подмены оконной процедуры (т. н. сабклассинга) существующего контрола. Используется макросом DDX_CONTROL, но вы можете вызывать этот метод и сами.
DWORD GetBitmapButtonExtendedStyle() | Возвращает набор расширенных стилей.
DWORD SetBitmapButtonExtendedStyle(DWORD dwExtendedStyle, DWORD dwMask = 0) | Устанавливает набор расширенных стилей.
HIMAGELIST GetImageList() | Возвращает хэндл списка изображений.
HIMAGELIST SetImageList(HIMAGELIST hImageList) | Устанавливает список изображений.
bool GetToolTipText(LPTSTR lpstrText, int nLength) | Возвращает текст всплывающей подсказки.
bool SetToolTipText(LPCTSTR lpstrText) | Устанавливает текст всплывающей подсказки (память для него динамически распределяется внутри класса CBitmapButton).
void SetImages(int nNormal, int nPushed = -1, int nFocusOrHover = -1, int nDisabled = -1) | Устанавливает соответствие состояний кнопки и изображений в списке. Используйте -1, чтобы оставить соответствующее состояние без изменений. Вы должны задать, по меньшей мере, изображение для состояния nNormal (отпущена), иначе программа не будет работать корректно. Замечу, что вы можете назначить нескольким состояниям одно и то же изображение.
BOOL SizeToImage() | Принудительно масштабирует кнопку по размеру связанных с ней изображений.
void DoPaint(CDCHandle dc) | Эта функция рисует кнопку в одном из четырёх состояний. Вам вряд ли понадобится вызывать её напрямую; зато её можно переопределить в производном классе и придать кнопке нестандартный вид, сохранив при этом остальную функциональность, которую предоставляет класс CBitmapButton.
Класс CCheckListViewCtrl

Из названия может показаться, что этот класс реализует список с <галочками designtimesp=13832> (check boxes), но это не совсем так. Стандартный контрол ListView уже поддерживает галочки. Достаточно задать ему расширенный стиль LVS_EX_CHECKBOXES. Что касается класса CCheckListViewCtrl, то он позволяет манипулировать несколькими галочками одновременно. Для этого пользователь выделяет несколько элементов в списке (используя Shift и Ctrl, в списке можно довольно быстро пометить нужную группу элементов). После этого щелчок по галочке любого элемента (или нажатие на Space) будет приводить к изменению состояния галочек у всех выделенных элементов. При необходимости такое поведение контрола можно подавить, удерживая Ctrl (при этом список будет вести себя, как обычный ListView).

Реализация класса CCheckListViewCtrl достаточно очевидна. Метод SubclassWindow подменяет оконную процедуру списка и принудительно устанавливает ему стиль LVS_EX_CHECKBOXES. Всю остальную работу делают обработчики сообщений WM_LBUTTONDOWN, WM_LBUTTONDBLCLK и WM_KEYDOWN. Все они используют для переключения галочек вспомогательную функцию CheckSelectedItems. Вы можете вызывать эту функцию и сами, хотя такая необходимость возникает нечасто. Функция CheckSelectedItems получает единственный параметр - номер элемента (этот элемент должен быть выделен). Она считывает состояние его галочки, инвертирует это состояние и применяет ко всем выделенным элементам в списке.

Резюмируя сказанное выше, для применения класса CCheckListViewCtrl в большинстве случаев достаточно просто связать объект этого класса с контролом, используя макрос DDX_CONTROL.

Класс CHyperLink

Класс CHyperLink предназначен для создания гиперссылок. На самом деле, большую часть функциональности он наследует от базового класса CHyperLinkImpl. Гиперссылка создаётся на основе статического элемента управления.

Класс CHyperLink наглядно демонстрирует, что иногда для решения самых простых задач приходится написать множество строк кода. Если, конечно, учесть разные <мелочи designtimesp=13856>, о которых задумываются далеко не все. Вот список основных возможностей класса.

  • Гиперссылка выглядит, как в IE. Интересно, что цвета для ссылки (обычной и посещённой) берутся из настроек IE, хранящихся в реестре. Если настройки обнаружить не удаётся, используются цвета по умолчанию (синий и фиолетовый).
  • При наведении на ссылку курсор приобретает форму руки. Обратите внимание, что под Windows 2000 класс CHyperLink использует системный курсор, а в других версиях Windows создаёт его <на лету designtimesp=13861>, избавляя вас от необходимости включать его в ресурсы во всех случаях. Кроме того, следует отметить, что курсор должен располагаться именно на надписи, а не в любой точке статического контрола, даже если вы нарисовали его очень большим.
  • Если задержать курсор над ссылкой, появляется всплывающая подсказка с адресом, по которому будет осуществляться переход.
  • Класс CHyperLink поддерживает не только <мышиный designtimesp=13865>, но и клавиатурный интерфейс. Ссылка, на которую установлен фокус ввода, выделяется пунктирной рамкой. При этом можно перейти по ней, нажав Enter или Space.
    ПРИМЕЧАНИЕ
    В типичном модальном диалоге Enter не будет попадать в ссылку, так как диалог преобразует его в нажатие кнопки по умолчанию. При желании изменить это поведение можно, переопределив обработчик OnGetDlgCode класса CHyperLink.
  • Переход по ссылке реализован через функцию ShellExecute. Эта функция понимает как адреса сайтов (при этом открывается броузер), так и почтовые адреса (при этом запускается почтовый клиент).

Список методов класса CHyperLink приведён в таблице 8.

Метод | Описание
CHyperLinkImpl() | Конструктор. Записывает в переменные объекта значения по умолчанию.
~CHyperLinkImpl() | Деструктор. Освобождает ресурсы, распределённые в процессе инициализации.
bool GetLabel(LPTSTR lpstrBuffer, int nLength) const | Возвращает метку гиперссылки (то есть строку, которую пользователь видит на экране).
bool SetLabel(LPCTSTR lpstrLabel) | Устанавливает метку гиперссылки. Текст сохраняется внутри объекта класса. Память для него распределяется динамически. По умолчанию в качестве метки используется содержимое статического контрола, который связывается с объектом класса. Поэтому часто удаётся обойтись и без метода SetLabel.
bool GetHyperLink(LPTSTR lpstrBuffer, int nLength) const | Возвращает адрес гиперссылки, по которому осуществляется фактический переход.
bool SetHyperLink(LPCTSTR lpstrLink) | Устанавливает адрес гиперссылки. Адрес также сохраняется внутри объекта класса в динамически распределяемой памяти. Иногда адрес совпадает с меткой. В этом случае устанавливать его вызовом SetHyperLink необязательно, так как класс CHyperLink сделает это за вас.
BOOL SubclassWindow(HWND hWnd) | Подменяет оконную процедуру окна, подключая к нему объект класса.
bool Navigate() | Осуществляет переход по ссылке.
void Init() | Инициализирует объект класса: создаёт или загружает курсор в виде руки, создаёт <подчёркнутый designtimesp=13953> фонт и тултип для ссылки, загружает из реестра цвета, которые использует IE.
void DoPaint(CDCHandle dc) | Рисует ссылку. Вы можете переопределить этот метод в производном классе, чтобы изменить её внешний вид.
ПРИМЕЧАНИЕ
Обратите внимание: если метка и адрес гиперссылки у вас отличаются, метод SetHyperLink следует вызывать до связывания объекта класса с контролом. Дело в том, что в момент связывания (в функции Init, которая вызывается из SubclassWindow) для ссылки создаётся тултип, в который записывается адрес ссылки. Если адрес ещё не задан, в тултип запишется метка.
Класс CMultiPaneStatusBarCtrlImpl

Класс CMultiPaneStatusBar призван облегчить вашу жизнь при работе со строками состояния. Стандартный контрол status bar из набора общих контролов Windows позволяет создать на строке состояния до 256 панелей, в которых можно отображать текст и иконки. Но он не предоставляет никаких средств для автоматического перемещения этих панелей. Программисту на <чистом designtimesp=13978> API приходится передвигать их вручную всякий раз, когда строка состояния изменяет свой размер. В MFC эту работу берёт на себя класс CStatusBar. А в WTL вам поможет класс CMultiPaneStatusBar.

Посмотрим, каким образом используется класс CMultiPaneStatusBar. Сначала объект класса связывается с существующей строкой состояния при помощи DDX_CONTROL. Можно и создать строку состояния с нуля, используя метод Create. Затем задаётся набор панелей для строки состояния. Для этого предназначен метод SetPanes. Он принимает количество панелей и массив с их идентификаторами. Идентификаторы используются для последующего обращения к панелям. Одной из панелей можно назначить стандартный идентификатор ID_DEFAULT_PANE. Панель с таким идентификатором растягивается, занимая всё свободное пространство в строке состояния. Остальные панели имеют фиксированный размер (который всегда можно изменить, используя метод SetPaneWidth). О корректном перемещении панелей заботится WTL. Вам остаётся только изменять текст панелей, их иконки и всплывающие подсказки в соответствии с вашими нуждами.

Полный список методов класса CMultiPaneStatusBar приведён в таблице 9.

Метод Описание
CMultiPaneStatusBarCtrlImpl()

Конструктор. Не выполняет никакой полезной работы.
~CMultiPaneStatusBarCtrlImpl()

Деструктор. Освобождает память, занятую списком идентификаторов панелей.
HWND Create(HWND hWndParent, LPCTSTR lpstrText, DWORD dwStyle, UINT nID)

HWND Create(HWND hWndParent, UINT nTextID, DWORD dwStyle, UINT nID)

Создают строку состояния.
BOOL SetPanes(int* pPanes, int nPanes, bool bSetText = true)

Задаёт набор панелей для строки состояния. При этом предыдущий набор полностью теряется. Массив pPanes содержит идентификаторы панелей. Начальный размер панели подгоняется под строку из ресурсов, идентификатор которой совпадает с идентификатором панели. Если задан флаг bSetText, текст из ресурсов будет сразу же вставлен в соответствующие панели.
bool GetPaneTextLength(int nPaneID, int* pcchLength = NULL, int* pnType = NULL) const

Возвращает длину текста в панели nPaneID через указатель pcchLength. По адресу pnType записывается тип панели. В Windows определены следующие типы: SBT_NOBORDERS (панель не имеет видимой рамки), SBT_OWNERDRAW (панель отрисовывается родительским окном), SBT_POPOUT (панель выглядит выпуклой на строке состояния) и SBT_RTLREADING (изменяет направление текста на противоположное). Нулевой тип соответствует обычной панели, которая <вдавлена designtimesp=14043> в строку состояния.

BOOL GetPaneText(int nPaneID, LPTSTR lpstrText, int* pcchLength = NULL, int* pnType = NULL) const

Аналогичен предыдущему, но извлекает также текст панели в буфер lpstrText.
BOOL SetPaneText(int nPaneID, LPCTSTR lpstrText, int nType = 0)

Задаёт текст (параметр lpstrText) и тип (параметр nType) для панели nPaneID.
BOOL SetPaneWidth(int nPaneID, int cxWidth)

Устанавливает ширину панели nPaneID равной cxWidth.
BOOL GetPaneTipText(int nPaneID, LPTSTR lpstrText, int nSize) const

Извлекает текст всплывающей подсказки для панели nPaneID.
BOOL SetPaneTipText(int nPaneID, LPCTSTR lpstrText)

Устанавливает текст всплывающей подсказки для панели nPaneID.
BOOL GetPaneIcon(int nPaneID, HICON& hIcon) const

Извлекает хэндл иконки, назначенной панели nPaneID.
BOOL SetPaneIcon(int nPaneID, HICON hIcon)

Задаёт иконку для панели nPaneID.
BOOL UpdatePanesLayout()

Пересчитывает расположение панелей. Вызывайте этот метод всякий раз, когда вы изменяете размеры панелей с помощью методов SetPanes и SetPaneWidth.
int GetPaneIndexFromID(int nPaneID) const

Определяет индекс панели по её идентификатору. Как известно, стандартный status bar использует для работы с панелями индексы. Вам вряд ли потребуется этот метод, поскольку класс CMultiPaneStatusBar позволяет вам выполнять все необходимые операции по идентификатору панели. Но для полноты картины стоит упомянуть и его.
ПРИМЕЧАНИЕ
Методы GetPaneTipText, SetPaneTipText, GetPaneIcon и SetPaneIcon доступны, только если макрос _WIN32_IE имеет значение 0x0400 или выше.

И последний важный момент. Всякий раз, когда окно изменяет размер, вы должны посылать строке состояния сообщение WM_SIZE, чтобы она могла скорректировать своё местоположение и размер.

Класс CWaitCursor

Класс CWaitCursor - это простенькая обёртка вокруг метода SetCursor из Win32 API. При помощи этого класса вы можете временно изменить вид курсора мыши. Чаще всего класс CWaitCursor применяют, чтобы "выплюнуть" песочные часы на время выполнения длительной операции. Отсюда и название класса.

Полный список методов класса CWaitCursor приведён в таблице 8.

Метод | Описание
CWaitCursor(bool bSet = true, LPCTSTR lpstrCursor = IDC_WAIT, bool bSys = true) | Конструктор. Параметр lpstrCursor задаёт имя ресурса, из которого следует грузить курсор. Если вы собираетесь использовать системный курсор, параметр bSys устанавливается в true. Наконец, флаг bSet определяет, следует ли вызывать из конструктора метод Set (см. ниже).
~CWaitCursor() | Деструктор. Из него принудительно вызывается метод Restore (см. ниже).
bool Set() | Заменяет текущий курсор курсором, заданным в конструкторе.
bool Restore() | Восстанавливает старый курсор, который был изменён методом Set.

Предлагаемые по умолчанию параметры конструктора "заточены" для индикации длительной операции. Использование класса CWaitCursor в этом случае тривиально:

void LengthyOperation()
{
    // Конструктор объекта waitCur вызовет метод Set, и курсор поменяется на "песочные часы".
    CWaitCursor waitCur;

    // Выполняем длительную операцию.
    ...

    // Здесь вызывается деструктор для объекта waitCur, и курсор восстанавливается.
}
Класс COwnerDraw<>: отрисовка контрола родительским окном в стиле WTL

Механизм отрисовки контрола родительским окном (owner draw) появился довольно давно - ещё в Windows 3.0. Он позволяет придать контролу совершенно произвольный внешний вид. Его поддерживают такие стандартные элементы управления, как кнопка, меню, простой список и комбинированный список.

В основе механизма owner draw лежат сообщения WM_DRAWITEM, WM_MEASUREITEM, WM_COMPAREITEM и WM_DELETEITEM. Так, в обработчике WM_DRAWITEM выполняется собственно отрисовка контрола, а в обработчике WM_MEASUREITEM - задание размеров отдельных элементов, содержащихся в контроле (пунктов меню, элементов списка и т. п.). WTL содержит небольшой класс COwnerDraw<>, который помогает вам обрабатывать все эти сообщения (описан в файле atlframe.h). Чтобы им воспользоваться, включите его в список базовых классов окна, которое будет заниматься отрисовкой контролов.

Посмотрим, какие элементы входят в класс COwnerDraw<>. В первую очередь это карта сообщений. Точнее, две карты (вы ещё не забыли, что в WTL окно может иметь несколько карт сообщений?).

    BEGIN_MSG_MAP(COwnerDraw< T >)
        MESSAGE_HANDLER(WM_DRAWITEM, OnDrawItem)
        MESSAGE_HANDLER(WM_MEASUREITEM, OnMeasureItem)
        MESSAGE_HANDLER(WM_COMPAREITEM, OnCompareItem)
        MESSAGE_HANDLER(WM_DELETEITEM, OnDeleteItem)
    ALT_MSG_MAP(1)
        MESSAGE_HANDLER(OCM_DRAWITEM, OnDrawItem)
        MESSAGE_HANDLER(OCM_MEASUREITEM, OnMeasureItem)
        MESSAGE_HANDLER(OCM_COMPAREITEM, OnCompareItem)
        MESSAGE_HANDLER(OCM_DELETEITEM, OnDeleteItem)
    END_MSG_MAP()

По умолчанию используется карта с номером 0. Она обрабатывает сообщения в родительском окне. Карту с номером 1 можно использовать для перехвата отражённых сообщений, связанных с механизмом owner draw, в самом контроле.

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

void DrawItem(LPDRAWITEMSTRUCT);
void MeasureItem(LPMEASUREITEMSTRUCT);
int CompareItem(LPCOMPAREITEMSTRUCT);
void DeleteItem(LPDELETEITEMSTRUCT);

Именно эти функции вы можете переопределить в производном классе, чтобы реализовать отрисовку контрола. Это удобнее, чем вручную перехватывать сообщения и вспоминать, каким образом в их параметрах запакована информация. Обратите внимание, что класс COwnerDraw<> содержит стандартную реализацию этих функций. Функции DrawItem, CompareItem и DeleteItem ничего полезного не делают, зато функция MeasureItem возвращает размер пункта меню в зависимости от настроек системы и размер элемента в списке в зависимости от размера стандартного системного фонта, который используется в диалогах и меню. Если такое поведение вас не устраивает, измените его на любое другое.

Рассмотрим пример использования класса COwnerDraw<> для рисования нестандартной кнопки.

class CButtonDemoDlg : public CSimpleDialog<IDD_BUTTON_DIALOG>, public COwnerDraw<CButtonDemoDlg>, ...
{
private:
    HICON m_hIcon1, m_hIcon2;
    ...

public:
    BEGIN_MSG_MAP(CButtonDemoDlg)
        ...
        CHAIN_MSG_MAP(COwnerDraw<CButtonDemoDlg>)
    END_MSG_MAP()

    void DrawItem(LPDRAWITEMSTRUCT pDIS)
    {
        if((pDIS->itemState & ODS_SELECTED) != 0)
        {
            // Кнопка нажата
            DrawIcon(pDIS->hDC, 0, 0, m_hIcon2);
        }
        else
        {
            // Кнопка отпущена
            DrawIcon(pDIS->hDC, 0, 0, m_hIcon1);
        }
    }
};
Класс CCustomDraw<>: пользовательское рисование в стиле WTL

Механизм пользовательского рисования (custom draw) иногда путают с owner draw. Он предназначен для той же цели - изменить внешний вид контролов. Однако он появился несколько позже (вместе с набором общих контролов из библиотеки comctl32.dll) и используется для более новых контролов (таких, как ListView и TreeView).

Пользовательское рисование работает следующим образом. Когда контрол перерисовывается, он посылает родительскому окну одно или несколько уведомлений NM_CUSTOMDRAW, упакованных в сообщение WM_NOTIFY. Каждое уведомление соответствует некоторой фазе перерисовки (до/после рисования контрола целиком или отдельного элемента и т. д.). Фазу можно определить по полю dwDrawStage структуры NMCUSTOMDRAW, указатель на которую передаётся вместе с уведомлением. В зависимости от фазы родительское окно может выполнить некоторые действия (например, изменить цвет или фонт отдельного элемента списка). Подробности можно найти в MSDN (см. статью "Customizing a Control's Appearance Using Custom Draw").

В WTL есть класс CCustomDraw<> (описан в файле atlctls.h), который помогает вам перехватывать уведомление NM_CUSTOMDRAW и распаковывать его параметры. Он очень похож на класс COwnerDraw<>, который мы рассмотрели выше. Его реализация выглядит так.

template <class T>
class CCustomDraw
{
public:
// Message map and handlers
    BEGIN_MSG_MAP(CCustomDraw< T >)
        NOTIFY_CODE_HANDLER(NM_CUSTOMDRAW, OnCustomDraw)
    ALT_MSG_MAP(1)
        REFLECTED_NOTIFY_CODE_HANDLER(NM_CUSTOMDRAW, OnCustomDraw)
    END_MSG_MAP()

// message handler
    LRESULT OnCustomDraw(int idCtrl, LPNMHDR pnmh, BOOL& bHandled)
    {
        T* pT = static_cast<T*>(this);
        pT->SetMsgHandled(TRUE);
        LPNMCUSTOMDRAW lpNMCustomDraw = (LPNMCUSTOMDRAW)pnmh;
        DWORD dwRet = 0;

        switch(lpNMCustomDraw->dwDrawStage)
        {
        case CDDS_PREPAINT:
            dwRet = pT->OnPrePaint(idCtrl, lpNMCustomDraw);
            break;
        case CDDS_POSTPAINT:
            dwRet = pT->OnPostPaint(idCtrl, lpNMCustomDraw);
            break;

        // Остальные фазы отрисовки
        // ...

        default:
            pT->SetMsgHandled(FALSE);
            break;
        }

        bHandled = pT->IsMsgHandled();
        return dwRet;
    }

// Overrideables
    DWORD OnPrePaint(int /*idCtrl*/, LPNMCUSTOMDRAW /*lpNMCustomDraw*/)
    {
        return CDRF_DODEFAULT;
    }
    DWORD OnPostPaint(int /*idCtrl*/, LPNMCUSTOMDRAW /*lpNMCustomDraw*/)
    {
        return CDRF_DODEFAULT;
    }
    // Остальные функции.
    // ...

Как видим, в классе CCustomDraw<> также предусмотрено две карты сообщений - для родительского окна и для самого контрола, если он получает отражённые уведомления. Обработчик OnCustomDraw распаковывает параметры уведомления NM_CUSTOMDRAW и определяет фазу рисования. Каждой фазе соответствует своя функция, которая и вызывается из OnCustomDraw. Вы можете переопределить любую из этих функций в производном классе и включить в неё нужный вам код (реализации из класса CCustomDraw<> не выполняют никой полезной работы). Список фаз рисования и соответствующих им функций приведён в таблице 10.

Фаза | Прототип функции
CDDS_PREPAINT | DWORD OnPrePaint(int idCtrl, LPNMCUSTOMDRAW lpNMCustomDraw)
CDDS_POSTPAINT | DWORD OnPostPaint(int idCtrl, LPNMCUSTOMDRAW lpNMCustomDraw)
CDDS_PREERASAE | DWORD OnPreErase(int idCtrl, LPNMCUSTOMDRAW lpNMCustomDraw)
CDDS_POSTERASE | DWORD OnPostErase(int idCtrl, LPNMCUSTOMDRAW lpNMCustomDraw)
CDDS_ITEMPREPAINT | DWORD OnItemPrePaint(int idCtrl, LPNMCUSTOMDRAW lpNMCustomDraw)
CDDS_ITEMPOSTPAINT | DWORD OnItemPostPaint(int idCtrl, LPNMCUSTOMDRAW lpNMCustomDraw)
CDDS_ITEMPREERASE | DWORD OnItemPreErase(int idCtrl, LPNMCUSTOMDRAW lpNMCustomDraw)
CDDS_ITEMPOSTERASE | DWORD OnItemPostErase(int idCtrl, LPNMCUSTOMDRAW lpNMCustomDraw)

Вот небольшой пример использования класса CCustomDraw<>. Для разнообразия я поручил обработку сообщения NM_CUSTOMDRAW самому контролу. Подразумевается, что родительское окно переправляет ему уведомления, используя механизм отражения.

class CCustomDrawListView : public CWindowImpl<CCustomDrawListView, CListViewCtrl>,
                            public CCustomDraw<CCustomDrawListView>
{
public:
    BEGIN_MSG_MAP(CCustomDrawListView)
        // Направляем сообщения в карту ?1 класса CCustomDraw!
        CHAIN_MSG_MAP_ALT(CCustomDraw<CCustomDrawListView>, 1)
    END_MSG_MAP()

    DWORD OnPrePaint(int idCtrl, LPNMCUSTOMDRAW lpNMCustomDraw)
    {
        // Запрашиваем уведомления NM_CUSTOMDRAW для каждого элемента списка.
        return CDRF_NOTIFYITEMDRAW;
    }

    DWORD OnItemPrePaint(int idCtrl, LPNMCUSTOMDRAW lpNMCustomDraw)
    {
        // Нам нужны поля, специфичные для ListView.
        LPNMLVCUSTOMDRAW pLVCD = (LPNMLVCUSTOMDRAW)lpNMCustomDraw;

        if((lpNMCustomDraw->dwItemSpec & 0x01) != 0)
        {
            // Для нечётных элементов: рисуем белым по чёрному.
            pLVCD->clrText = RGB(255,255,255);
            pLVCD->clrTextBk = RGB(0,0,0);
        }
        else
        {
            // Для чётных элементов: рисуем красным по серому.
            pLVCD->clrText = RGB(255,0,0);
            pLVCD->clrTextBk = RGB(200,200,200);
        }

        return CDRF_NEWFONT;
    }
};
От теории к практике

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

WTLErrLook: приложение на базе модального диалога
Демонстрационный проект WTLErrLook

WTLErrLook

Приложение WTLErrLook - это упрощённый вариант программы Error Lookup, которая входит в Visual Studio 6. Главное окно программы выполнено в виде модельного диалога. Обмен данными с полями ввода осуществляется с помощью DDX_TEXT.

WTLSndVol: приложение на базе немодального диалога
Демонстрационный проект WTLSndVol

WTLSndVol

WTLSndVol - это упрощённая версия регулятора громкости (sndvol32.exe), который входит в комплект Windows. При запуске программы она не показывает главное окно (которое выполнено в виде немодального дмалога), а размещает иконку в системном трее (Shell_NotifyIcon). Чтобы она отличалась от иконки стандартного регулятора, я сделал её зелёной. Щелчок по иконке приводит к появлению окна регулятора. Для изменения громкости используется класс CSimpleMixer. Рассматривать его устройство мы не будем, так как это тема для отдельной статьи. Чтобы закрыть WTLSndVol, щёлкните правой кнопкой на иконке в трее и выберите из меню команду Exit.

WTLNavigator: использование диалогов с ActiveX-контролами
Демонстрационный проект WTLNavigator

WTLNavigator

WTLNavigator - это примитивный броузер, построенный на основе ActiveX-контрола "Web Browser". Класс главного окна приложения унаследован от класса CAxDialogImpl.

WTLCalc: обновление дочерних окон
Демонстрационный проект WTLCalc

WTLCalc

WTLCalc - это простенький калькулятор. Доступность математических операций в калькуляторе зависит от введённого числа: логарифм может применяться только к положительным числам, факториал - только к натуральным и т. д. Соответственно, для включения и выключения кнопок используется механизм CUpdateUI.

WTLSizeDlg: пример масштабируемого диалога
Демонстрационный проект WTLSizeDlg

WTLSizeDlg

Программа WTLSizeDlg не выполняет никакой полезной работы. Она просто рисует диалог и позволяет его масштабировать. Для поддержки масштабирования используется класс CDialogResize. Обратите внимание, что корректное масштабирование контролов обеспечивается благодаря наличию невидимого контрола.

WTLCtlDemo: использование стандартных и общих контролов
Демонстрационный проект WTLCtlDemo

WTLCtlDemo

Программа WTLCtlDemo показывает, как можно работать со стандартными контролами - static, button, edit box, list box, combo box, list view и tree view.

WTLCtlxDemo: использование "самодельных" контролов WTL
Демонстрационный проект WTLCtlxDemo

WTLCtlxDemo

Программа WTLCtlxDemo демонстрирует применение <самодельных designtimesp=14426> контролов, предоставляемых библиотекой WTL - CBitmapButton, CHyperLink, CCheckListViewCtrl и CMultiPaneStatusBarCtrl.

Это все на сегодня. Пока!

Алекс Jenter jenter@rsdn.ru
Duisburg, 2001. Публикуемые в рассылке материалы принадлежат сайту RSDN.


ПРОГРАММИРОВАНИЕ НА V I S U A L C + +
РАССЫЛКА САЙТА
RSDN.RU
Выпуск No.56 от 2 декабря 2001 г.

РАССЫЛКА ЯВЛЯЕТСЯ ЧАСТЬЮ ПРОЕКТА RSDN , НА САЙТЕ КОТОРОГО ВСЕГДА МОЖНО НАЙТИ ВСЮ НЕОБХОДИМУЮ РАЗРАБОТЧИКУ ИНФОРМАЦИЮ, СТАТЬИ, ФОРУМЫ, РЕСУРСЫ, ПОЛНЫЙ АРХИВ ПРЕДЫДУЩИХ ВЫПУСКОВ РАССЫЛКИ И МНОГОЕ ДРУГОЕ.

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

CТАТЬЯ

Поиск в MSDN
Автор: Александр Шаргин

MSDN - это библия Windows-разработчика. В ней содержится огромное количество важной и полезной информации по всем основным продуктам и технологиям фирмы Microsoft. Но когда информации накапливается слишком много, встаёт другая проблема - проблема поиска именно тех данных, которые требуются в данный момент. В этой статье мы поговорим о том, как искать нужную информацию в MSDN.

ПРИМЕЧАНИЕ
Далее речь пойдёт о той версии MSDN, которая распространяется на дисках и используется в качестве хелпа в среде разработки Visual C++. Тем не менее, некоторые рекомендации можно с успехом применять при поиске в Интернете на сайте MSDN Online.
Вкладка Search

Базовым инструментом для поиска информации в MSDN служит вкладка Search навигационной панели. Посмотрим, какие средства она нам предоставляет.

Вкладка Search

В верхней части вкладки Search находится поле ввода "Type in word(s) to search for". Оно позволяет вводить и выполнять запросы на поиск информации. Искать можно в статьях MSDN, в заголовках статей (флажок "Search titles only") или в статьях, найденных по предыдущему поисковому запросу (флажок "Search previous results"). Чтобы выполнить запрос, нажмите Enter (или щёлкните по кнопке "List Topics"). Появится список статей, удовлетворяющих запросу. Перейти к любой статье в списке можно, дважды щёлкнув по ней левой клавишей мыши. Можно также выделить нужную статью и щёлкнуть по кнопке "Display". Для удобства список статей можно сортировать по названию, разделу или рангу (то есть по степени соответствия запросу). Для этого нужно щёлкнуть по заголовку соответствующего столбца списка.

СОВЕТ
Если вы нашли статью, которая вас заинтересовала, посмотрите, где она находится в оглавлении (для этого нужно нажать кнопку Locate на панели инструментов). Возможно, рядом с ней вы найдёте несколько похожих статей, которые вам пригодятся, или даже целый раздел, посвящённый интересующей вас теме.
Синтаксис запросов

В простейшем случае запрос может состоять из одного-единственного слова. Результатом будет список статей, в которых это слово встречается. При поиске не учитывается регистр букв, поэтому запросы "style", "STYLE" и даже "StYlE" дадут одинаковый результат. Слова, которые вы вводите, могут состоять из любых букв и цифр. Использовать знаки препинания допускается, но они игнорируются при поиске.

Введённое вами слово разыскивается именно в той грамматической форме, в которой вы его набрали. Если вы хотите, чтобы учитывались все возможные формы слова, установите флажок "Match similar words" в нижней части вкладки Search. Если установить этот флажок, то по запросу "develop" будут также обнаружены статьи со словами "developed", "developer" и даже "development".

Можно искать в MSDN не только фиксированное слово, но и фиксированную фразу. Для этого фразу нужно заключить в двойные кавычки. Как и в случае с отдельными словами, можно использовать флажок "Match similar words" для поиска фразы во всех возможный грамматических формах. Если установить этот флажок, то запрос "create dialog" отыщет также статьи с фразами "creating dialog", "created dialog", "create dialogs" и т. п.

В словах можно использовать специальные символы "*" и "?". Они имеют тот же смысл, что и в командном языке Windows: "?" обозначает одну произвольную букву, а "*" - любую последовательность букв. Например, вы можете быстро найти все статьи, в которых рассказывается об уведомлениях элемента ListView, задав запрос "LVN_*".

Для более тонкого отбора нужных статей используются операторы AND, OR, NOT и NEAR. Они вводятся прямо с клавиатуры или из меню, которое появляется при щелчке на стрелочке, расположенной справа от окна с запросом.

ПРЕДУПРЕЖДЕНИЕ
Операторы AND, OR и NOT нельзя заменять на позаимствованные из языка C символы &, | и !. Хотя в документации утверждается, что это допустимо, ваши запросы не будут работать.

С помощью операторов можно связывать как отдельные слова, так и заключённые в кавычки фиксированные фразы. Операторы AND, OR и NOT имеют стандартный смысл "И", "ИЛИ" и "НЕ", а оператор NEAR означает, что связанные им слова (фразы) должны находиться недалеко (в пределах 8 слов) друг от друга. Например, можно задать запрос "debug NEAR release NEAR build" для поиска статей об отличиях отладочной и финальной версии программы в Visual C++. Если вы опускаете оператор между словами, считается, что это оператор AND.

СОВЕТ
Обратите особое внимание на оператор NEAR. В MSDN содержатся десятки тысяч статей, многие из них очень большие. Даже если вы объедините несколько слов оператором AND, они могут встретиться в статье, которая совершенно не относится к интересующей вас теме. Использование NEAR существенно повышает вероятность найти именно то, что вам нужно.

Запросы с операторами всегда читаются слева направо. Никаких приоритетов для операторов не вводится. Чтобы изменить порядок обработки запроса, можно использовать круглые скобки.

Подсветка результатов поиска

Чтобы определить, насколько найденные статьи соответствуют нашим нуждам, часто бывает удобно включить подсветку слов или фраз, которые совпали с нашим запросом. Для этого нужно установить галочку Highlights в меню View.

Подмножества

MSDN часто ругают за то, что в ней вся информация свалена в кучу. Похожие функции есть и в Win32 API, и в MFC, и в Java, и в VB... Попробуйте ввести в индексе функцию типа "print", и вы поймёте, что я имею в виду. К счастью, нас никто не заставляет работать с библиотекой MSDN целиком. Вы можете определить в ней некоторое подмножество разделов, с которым будут работать оглавление, индекс и поиск. Используя подмножества, вы можете существенно сузить круг поиска нужной информации и быстрее найти то, что вам нужно.

Подмножество задаётся в выпадающем списке "Active Subset" в самой верхней части навигационной панели MSDN. Пункт "(Entire Collection)" соответствует всей библиотеке целиком. Кроме этого, в MSDN обычно присутствует несколько удобных предопределённых подмножеств, которыми вы можете воспользоваться. Например, подмножество "MSDN, Knowledge Base" ограничивает круги ваших поисков Базой Знаний фирмы Microsoft (о ней мы подробно поговорим в следующем разделе). А подмножество "Visual C++, Platform SDK, and Enterprise Docs" должно понравится разработчику на языке C++, так как содержит документацию на сам язык C++ и его стандартную библиотеку, среду Visual C++, Win32 API и библиотеки MFC и ATL.

Если предопределённых подмножеств недостаточно, всегда можно определить свои собственные. Для этого предназначен пункт меню "View0>Define Subset..." (определить подмножество).

Диалог Define Subset

В открывшемся диалоге "Define Subset" вы увидите два дерева. В правом показаны разделы, принадлежащие подмножеству, а в левом - не входящие в него. Раскрывать и сворачивать подразделы можно двойным щелчком левой клавиши мыши. Для перемещения разделов в подмножество и из него используйте кнопки Add, Add All, Remove и Remove All. Из списка "Select subset to display" выбирается подмножество, которое вы будете редактировать (по умолчанию выбирается "New" - новое подмножество). Здесь же можно удалить ненужное подмножество, выбрав его из списка и нажав Delete. Если вы создали новое подмножество, не забудьте назначить ему название в поле "Save new subset as" и сохранить его, нажав Save.

Поиск в Базе Знаний

База Знаний (Knowledge Base, KB) - это огромная коллекция технических документов. Эти документы дополняют документацию, описывая решения конкретных проблем, которые могут возникнуть у пользователя или программиста. В Базе Знаний содержатся ответы на многие вопросы, нужно только найти их там.

Каждая статья в KB содержит несколько специальных ключевых слов, которые её довольно точно классифицируют. Все эти слова начинаются с префикса kb. Задавая одно или несколько таких ключевых слов в поисковом запросе, можно вычленить из Базы именно те статьи, которые вам требуются. Список основных ключевых слов приведён в последующих разделах.

Категории статей

Каждая статья в Базе Знаний относится к одной из следующих категорий:

  • HOWTO. Статья описывает решение какой-либо задачи по шагам.
  • INFO. В статье содержится дополнительная информация по продуктам или технологиям фирмы Микрософт, которая не вошла в официальную документацию.
  • PRB. В статье описывается проблема, с которой вы можете столкнуться, и пути её обхода.
  • BUG. В статье задокументирован баг в продукте или технологии фирмы Микрософт.
  • FIX. Статья описывает баг, который присутствовал в предыдущей версии продукта или технологии, но исправлен в текущей версии.
  • SAMPLE. Законченная демонстрационная программа.
  • FAQ. Ответ на часто задаваемый вопрос.
  • DOC. Поправки или дополнения к официальной документации на продукты и технологии фирмы Микрософт.

Если вы ищете в Базе Знаний статьи определённой категории (категорий), вы можете использовать в запросе специальные ключевые слова.

Категория | Ключевое слово
HOWTO | kbHOWTO
INFO | kbINFO
PRB | kbPRB
BUG | kbBUG
FIX | kbFIX
SAMPLE | kbSAMPLE
FAQ | kbFAQ
DOC | kbDOC
Тематика

Ключевые слова, связанные с тематикой, можно комбинировать с ключевыми словами, соответствующими конкретным технологиям. Это позволит получить результаты, более точно соответствующие вашим нуждам. Например, если вас интересует безопасность в ASP, вы можете задать запрос "kbASP kbSecurity". Если вас интересует отладка DLL, используйте запрос "kbDLL kbDebug". И так далее.

Тематика | Ключевое слово
Отладка | kbDebug
Сообщения об ошибках | kbErrMsg
Скриптовые языки | kbScript
Незаконченные фрагменты кода (code snippets) | kbCodeSnippet
Использование визардов | kbwizard
Безопасность | kbSecurity
Производительность | kbPerformance
Масштабируемость | kbScalability
Развёртывание приложений | kbDeployment
Локализация | kbLocalization
Аппаратные платформы
Тема | Ключевое слово
Платформа Intel x86 | kbx86
Эмуляция Intel x86 | kbEmulatex86
DEC Alpha | kbDecAlpha
Компьютеры Macintosh | kbMAC
Платформы на базе чипов MIPS | kbMIPS
Процессоры PowerPC | kbPowerPC
Процессоры Hitachi SuperH | kbSuperH
Операционные системы
Тема | Ключевое слово
Всё семейство ОС Windows | kbOSWin, kbWinOS
ОС Windows NT | kbNTOS
ОС Windows NT 4.0 | kbNTOS400
ОС Windows 2000 | kbWinOS2000
ОС Windows 95 | kbWinOS95
ОС Windows 98 | kbWinOS98
ОС Windows CE | kbWinCE
Unix | kbOSUNIX, kbUNIX
Язык C++ и компилятор фирмы Microsoft
Тема | Ключевое слово
Только язык C (без C++) | kbConly
Только язык C++ | kbCPPonly
Синтаксис языка C | kbLangC
Синтаксис языка C++ | kbLangCPP
Библиотека C run-time (CRT) | kbCRT
Обработка исключений в C++ | kbExceptHandCPP
Шаблоны | kbtemplate
Standard Template Library (STL) | kbSTL
Код, генерируемый компилятором | kbCodeGen
Компилятор | kbCompiler
Линкер | kbLinker
Утилита Nmake | kbNMake
Интегрированная среда разработки
Тема | Ключевое слово
Общие |
Visual Studio (все версии) | kbVS
Visual Studio 97 | kbVS97
Visual Studio 6.0 | kbVS600
Среда разработки Developer Studio | kbDevStudio
Интегрированная среда разработки (IDE) | kbIDE
Visual C++ |
Visual C++ | kbVC
Visual C++, версия 6.0 | kbVC600
Объектная модель Visual C++ | kbVCObj
Редактор кода | kbEditor
Редактор ресурсов | kbResourceEd
ClassViewer в Visual C++ | kbClassView
Class Wizard | kbClassWizard
Галерея компонентов (Component Gallery) | kbCompGallery
Создание собственных визардов | kbCustomWizard
Другие среды |
Visual Basic | kbVBp
Visual Fox Pro | kbVFP
Visual InterDev | kbVisID
Visual Source Safe | kbSSafe
Инструментарий
Тема | Ключевое слово
Различные инструменты | kbMiscTools
Инструменты для анализа производительности | kbPerformanceTool
Продукты, поставляемые в составе Platform SDK | kbSDKPlatform
Windows Kernel Debugger (WinDBG.EXE) | kbWinDBG
Visual Analyzer | kbVisAnalyzer
Инсталляция
Тема | Ключевое слово
Инсталляция приложений на компьютер пользователя | kbAppSetup
Проблемы с установкой приложений фирмы Микрософт | kbSetup
Microsoft Windows Installer (MSI) | kbMSI
Microsoft Windows Installer, версия 1.0 | kbMSI100
Microsoft Windows Installer, версия 1.1 | kbMSI110
Visual Studio Installer | kbVSI
Win32 API
Тема | Ключевое слово
Весь Application Programming Interface (API) | kbAPI
Структурная обработка исключений в Win32 | kbExceptHandSEH
Реестр | kbRegistry
Многопоточность | kbThread
Динамически подключаемые библиотеки (DLL) | kbDLL
Программирование сервисов | kbService
Буфер обмена | kbClipboard
Файловый ввод/вывод | kbFileIO
Работа со строками и строковыми ресурсами | kbString
Дата и время | kbDateTime
Консоль управления фирмы Микрософт (Microsoft Management Console, MMC) | kbMMC
Система помощи в старом формате (WinHelp) | kbWinHelp
Система помощи на базе HTML | kbHTMLHelp
Общие контролы Windows | kbCmnCtrls
Стандартные диалоги | kbCmnDlg
Создание и использование диалоговых окон | kbDlg
Работа с меню | kbMenu
Программирование акселераторов ("горячих клавиш") | kbKeyAccel
Программирование Drag and Drop | kbDragDrop
Графика средствами GDI | kbGDI
Печать | kbPrinting
Сокеты | kbWinsock
COM/DCOM, COM+
Тема | Ключевое слово
Модель COM/DCOM |
Вся Component Object Model (COM) | kbCOMt
Distributed COM (DCOM) | kbDCOM
ODL-файлы | kbODL
IDL-файлы | kbIDL
Компилятор MIDL | kbMIDL
Внутрипроцессные COM-серверы | kbInprocSvr
Локальные COM-серверы | kbLocalSvr
Маршалинг | kbMarshal
Лицензирование | kbLicensing
Точки соединения (Connection Points) | kbConnPts
Подразделения | kbApartment
Технологии OLE и ActiveX |
Вся технология Object Linking and Embedding (OLE) | kbOLE
Все технологии ActiveX | kbActiveX
Длительное хранение (persistent storage) в OLE | kbPersistSt
Интерфейс IDataObject | kbDataObject
События | kbActiveXEvents
Составные документы (compound documents) | kbCmpDoc
Контейнеры OLE | kbContainer
Активизация "по месту" (inplace activation) | kbInplaceAct
Активные документы (active documents) | kbActiveDocs
Автоматизация приложений | kbAutomation
ActiveX-контролы (создание и использование) | kbCtrl
ActiveX-контролы (только создание) | kbCtrlCreate
COM+ и MTS |
Весь COM+ | kbCOMPLus
Microsoft Transaction Server (MTS) | kbMTS
Асинхронные вызовы COM+ | kbCOMPlusAsync
Контексты COM+ | kbCOMPlusContext
События COM+ | kbCOMPlusLCE
Базы данных
Тема | Ключевое слово
Все статьи, посвящённые базам данных | kbDatabase
Microsoft Data Access Components (MDAC) | kbMDAC
Open Database Connectivity (ODBC) | kbODBC
Java Database Connectivity (JDBC) | kbJDBC
Data Access Objects (DAO) | kbDAO
Data Access Object SDK | kbSDKDAO
Ядро JET | kbJET
Remote Data Objects (RDO) | kbRDO
Remote Data Service (RDS) | kbRDS
OLE Database Interface (OLEDB) | kbOLEDB
OLEDB-потребители | kbConsumer
OLEDB-провайдеры | kbProvider
Простой OLE DB-провайдер | kbOSP
ActiveX Data Objects (ADO) | kbADO
Microsoft SQL Server | kbSQLServ
Отладчик T-SQL | kbTSQL
Продукты и технологии Oracle | kbOracle
Exchange | kbXchge
Installable ISAM | kbIISAM
Технологии и формат dBASE | kbDBase
Excel | kbExcel
Access | kbAccess
Создание и распространение драйверов для баз данных | kbDriver
Интернет
Тема | Ключевое слово
Active Server Pages (ASP) | kbASP
XML | kbXML
Парсер MSMXL (все версии) | kbMSXML
Парсер MSMXL, версия 2.0 | kbMSXML200
Парсер MSMXL, версия 3.0 | kbMSXML300
Internet Service API (ISAPI) | kbISAPI
Веб-серверы | kbWebServer
Cookies | kbCookies
MFC
Тема | Ключевое слово
Вся библиотека Microsoft Foundation Classes (MFC) | kbMFC
Архитектура документ/представление | kbDocView
Многодокументный интерфейс (Multiple Document Interface, MDI) | kbMDI
Классы MFC, произведённые от CControlBar | kbMFCCtrlBar
Контекстно-зависимая справка | kbCSHelp
Программирование панелей инструментов | kbToolbar
Разработка пользовательского интерфейса | kbUIDesign
Обработка исключений в MFC | kbExceptHandMFC
ATL
Тема | Ключевое слово
Active Template Library (ATL) | kbATL
Ver. 1.1 Active Template Library, версия 1.1 | kbATL110
Ver. 2.0 Active Template Library, версия 2.0 | kbATL200
Ver. 2.1 Active Template Library, версия 2.1 | kbATL210
Ver. 3.0 Active Template Library, версия 3.0 | kbATL300
Поддержка окон в ATL | kbATLWC
Провайдеры и потребители в ATL | kbDTL
Прочее
Тема | Ключевое слово
Статьи, написанные группами технической поддержки Микрософт | kbDSupport
Общая информация, не относящаяся к программированию | kbGenInfo
Взаимодействие с MSDN | kbMSDN
Компоненты и драйверы третьих фирм | kb3rdparty

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

Как вызвать скрипт из приложения?
Автор: Тимофей Чадов

При использовании WebBrowser Вы можете вызывать любые скрипты, расположенные в теле html-страницы. Обращение к скриптам производится через диспетчерский интерфейс, возвращаемый в свойстве Script интерфейса IHTMLDocument.

В следующем примере демонстрируется вызов функции с именем evalute, которая определена в теле html-страницы следующим образом.

<SCRIPT>
function evaluate(x)
{
    alert(x +"= " + eval(x));
}
</SCRIPT>

// Эта функция выполняет скрипт
void CMyHtmlView::OnCallscript()
{
    HRESULT hr;
    LPDISPATCH pDispatch = GetHtmlDocument();
    if( pDispatch == NULL ) return;

    IHTMLDocument* pHtmlDoc;
    hr = pDispatch->QueryInterface( __uuidof( IHTMLDocument ), (void**)&pHtmlDoc );

    LPDISPATCH pScript;
    pHtmlDoc->get_Script( &pScript);
    pScript->AddRef();
    if (SUCCEEDED(hr))
    {
        // Получаем DISPID интересуемой функции
        OLECHAR* szMember = L"evaluate";
        DISPID dispid;

        HRESULT hr = pScript->GetIDsOfNames(IID_NULL, &szMember, 1,
            LOCALE_SYSTEM_DEFAULT, &dispid);

        if (SUCCEEDED(hr))
        {
            // Выполняем
            COleVariant vtResult;
            static BYTE parms[] = VTS_BSTR;

            COleDispatchDriver dispDriver(pScript);
            dispDriver.InvokeHelper(dispid, DISPATCH_METHOD, VT_VARIANT,
                (void*)&vtResult, parms,
                "5+Math.sin(9)");
        }
    }
    pScript->Realease();
    pHtmlDoc->Release();
    pDispatch->Release();
}

ФОРУМ RSDN - ИЗБРАННОЕ

Тема: Коллеги, улыбнитесь
От: adonz
...В продолжение темы:
  • Главная пpогpамма - функции malloc: Пpошу обеспечить выделение 257 килобайт Conventional Memory.
  • Функция malloc - опеpационной системе: Пpошу выделить 257 килобайт Conventional Memory в связи с пpоизводственной необходимостью.
  • Опеpационная система - главной пpогpамме: Выделить 257 килобайт Conventional Memory не пpедставляется возможным в связи с отсутствием таковых. Есть 3 мега Extended. Беpете?
  • Главная пpогpамма - обpаботчику исключений: С памятью облом. Что делать будем?
  • Обpаботчик исключений - пpоцедуpе оптимизации: Hеобходимо добиться экономии памяти за счет более pационального использования системных pесуpсов.
  • Контpоллеp пpеpываний - обpаботчику пpеpываний: Тут это... юзеp кнопку нажал...
  • Главная пpогpамма - обpаботчику пpеpываний: Hе деpгайся! Подеpжит и отпустит.
  • Пpоцедуpа оптимизации - обpаботчику исключений: Готово!
  • Главная пpогpамма - обpаботчику исключений: Hу что там?
  • Обpаботчик исключений - главной пpогpамме: Еще хуже стало. Может, на диск посвопимся?
  • Главная пpогpамма - жесткому диску: Пpошу пpинять на хpанение swap-файл в pазмеpе 257 килобайт.
  • Жесткий диск - главной пpогpамме: Ваша пpосьба не может быть удовлетвоpена за недостатком места.
  • Главная пpогpамма - опеpационной системе: Что еще за глюк? Было же место!
  • Жесткий диск - главной пpогpамме: Hичего не глюк. Вы еще пpошлый swap-файл на 4 мега не забpали. А я, между пpочим, не pезиновый. И даже не stacker'ный.
  • Контpоллеp пpеpываний - обpаботчику пpеpываний: Тут это... юзеp опять кнопку давит...
  • Обpаботчик пpеpываний - PC speaker'у: Hу скажи ему что-нибудь, пусть отвяжется!
  • PC speaker - юзеpу: Биип!
  • Главная пpогpамма - опеpационной системе: Hу может можно чего-нибудь пеpеpаспpеделить?
  • Опеpационная система - главной пpогpамме: Дpугим задачам тоже память нужна. Вам что, General Protection Error схлопотать охота?
  • Главная пpогpамма - хакеpской функции: Hу-ка выясни, кто там отожpал всю память, и выкини их к XTшной матеpи!
  • Контpоллеp пpеpываний - обpаботчику пpеpываний: Тут юзеp Ctrl-Alt-Del жмет!
  • Главная пpогpамма - обpаботчику пpеpываний: Да отpуби ты этому зануде клавиатуpу! Мы тут делом заняты...
  • Хакеpская функция - главной пpогpамме: Опаньки!
  • Главная пpогpамма - опеpационной системе: Hу что там? Сколько памяти свободно?
  • Опеpационная система - главной пpогpамме: 320 Conventional и... ой, куда это Extended Memory Manager делся?
  • Главная пpогpамма - хакеpской функции: Ты что начистила, сволочь?
  • Хакеpская функция - главной пpогpамме: А чо я, чо я? Мне сказали - выкинуть, я и выкидываю!
  • Функция malloc - опеpационной системе: Пpошу выделить 257 килобайт Conventional Memory в связи с пpоизводственной необходимостью.
  • Опеpационная система - главной пpогpамме: Hе могу. У меня в Extended Memory важные данные лежали. И вообще, я с вами скоpо повешусь!
  • Кнопка Reset - пpоцессоpу: Hу что? Доигpались?

  • ... ХОЛОДHЫЙ РЕСТАРТ...

  • Autoexec.bat - главной пpогpамме: Так на чем мы остановились?

Это все на сегодня. Пока!

Алекс Jenter jenter@rsdn.ru
Duisburg, 2001. Публикуемые в рассылке материалы принадлежат сайту RSDN.


ПРОГРАММИРОВАНИЕ НА V I S U A L C + +
РАССЫЛКА САЙТА
RSDN.RU
Выпуск No. 57 от 23 декабря 2001 г.

РАССЫЛКА ЯВЛЯЕТСЯ ЧАСТЬЮ ПРОЕКТА RSDN , НА САЙТЕ КОТОРОГО ВСЕГДА МОЖНО НАЙТИ ВСЮ НЕОБХОДИМУЮ РАЗРАБОТЧИКУ ИНФОРМАЦИЮ, СТАТЬИ, ФОРУМЫ, РЕСУРСЫ, ПОЛНЫЙ АРХИВ ПРЕДЫДУЩИХ ВЫПУСКОВ РАССЫЛКИ И МНОГОЕ ДРУГОЕ.

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

Опять я к сожалению заставил вас недоумевать, почему не выходит рассылка. Дело в том, что мне по личным причинам пришлось срочно поехать в другой город и пробыть там две недели. Выпускать рассылку оттуда в это время не было никакой возможности. Само собой разумеется, что теперь рассылка будет выходить вовремя. Я искренне прошу прощения и надеюсь на ваше понимание. Ну а теперь - к делу! Ведь за это время на сайте RSDN появилось много интересного...

CТАТЬЯ

GDI+
Часть 1. Краткое знакомство
Автор: Виталий Брусенцев
Демонстрационное приложение на C++ (требует наличия GDI+) - 88 Кб.
Демонстрационное приложение на C# (требует CLR) - 62 Кб.

За последний год компания Microsoft подготовила разработчикам множество сюрпризов. Новые продукты и технологии буквально завладели вниманием околокомпьютерного мира. Пока неясно, насколько успешным будет дебют технологии .NET и основанных на ней программных продуктов и средств разработки. Но одно из новшеств, безусловно, уже завоевало признание разработчиков, связанных с графикой и мультимедиа, - технология GDI+. Именно она, вернее, основанный на ней новый графический интерфейс является "лицом" новых операционных систем - Windows XP и .NET Server.

Что же такое GDI+? Официальная документация скромно называет ее Class-based API, то есть основанным на классах интерфейсе прикладных программ. Так как она встроена в Windows XP и .NET Server, ее называют частью этих операционных систем. Часто встречается также определение "библиотека" или "библиотека классов". В действительности, предоставляемый GDI+ набор классов является тонкой оболочкой над множеством обычных функций, реализованных в одной динамической библиотеке GdiPlus.dll. В общем, имея все это в виду, будем для краткости далее называть ее просто библиотекой.

Итак, GDI+ - это библиотека, призванная заменить существующий уже больше 11 (или 18 - как считать) лет интерфейс GDI, являющийся графическим ядром предыдущих версий Windows. Она сочетает в себе (по крайней мере, по замыслу) все достоинства своего предшественника и предоставляет множество новых мощных возможностей. Кроме того, при ее проектировании заранее ставилась цель наименее болезненного переноса приложений на 64-битные платформы. Следовательно, хотя существующие GDI-приложения будут выполняться на новых версиях Windows, для новых проектов следует использовать GDI+.

Заглянем "под капот"
Что новенького?

Далее мы еще будем рассматривать специфические (и такие эффектные!) возможности GDI+. Здесь же только опишем основные новшества.

Достоинства C++ - реализации:

  • Объектно-ориентированный интерфейс: благодаря поддержке компилятора C++ мы "бесплатно" получаем контроль над типами и временем жизни объектов.
  • Прозрачное управление памятью: объекты ядра GDI+ создаются в куче с помощью собственного менеджера памяти прозрачно для программиста.
  • Использование перегрузки имен функций: функции одного назначения различаются только по своим параметрам.
  • Собственное пространство имен: позволяет использовать понятные имена типов - такие, как Rect, Pen и Matrix - без конфликтов с другими библиотеками.
  • Перегрузка операторов: предоставляет удобные операции '+' и '-' для таких типов, как Point и Size.

Архитектурные новинки библиотеки:

  • Аппаратная абстракция: как уже было замечено, упрощается перенос на 64-битные платформы.
  • Новый дизайн графических функций/объектов: теперь можно не бояться "оставить выбранной кисть в контексте перед удалением" - такая типичная для GDI ошибка!
  • Разделение функций закраски и отрисовки: предоставляет большую гибкость в рисовании, например, позволяет заливать незамкнутые фигуры.
  • Появление графических контейнеров: контейнеры позволяют "сцепить" вместе несколько операций и использовать как одну команду.
  • Увеличившаяся поддержка путей (paths) и их взаимодействия с регионами: теперь пути являются полноправными объектами вне контекста рисования и могут легко трансформироваться в регионы.

Новые технологии и возможности (задержите дыхание):

  • Градиентная закраска: позволяет заливать сложные фигуры оттенками с различными законами распределения цвета, рисовать векторные примитивы (например, линии) с градиентной окраской.
  • Поддержка прозрачности: можно создавать кисти и растры с прозрачными и полупрозрачными областями, заливать области полупрозрачным цветом, назначать Color Key для растрового изображения и работать с его альфа-каналом, а также рисовать полупрозрачные (!) векторные примитивы и текст.
  • Режимы улучшения изображения: позволяют значительно улучшить пользовательское восприятие за счет сглаживания контурных неровностей (antialiasing) и префильтрации растровых изображений.
  • Сплайны: кроме уже существующих в GDI кривых Безье, поддерживается новый вид кривых - так называемые сплайны, которые имитируют поведение натянутой и изогнутой стальной полосы. Сплайны являются гладкими кривыми.
  • Пути: как уже говорилось, пути теперь существуют независимо от контекста рисования и представляют собой мощное средство создания сложных векторных объектов. Кроме того, появилась возможность выравнивать (flatten) пути, то есть преобразовывать их к набору отрезков прямых.
  • Координатные преобразования: объект Matrix позволяет осуществлять операции поворота, переноса, масштабирования и отражения объектов GDI+.
  • Регионы: в отличие от GDI, регионы теперь не привязаны к координатам устройства и подчиняются координатным преобразованиям.
  • Работа с растрами: теперь можно практически все! Поддерживается отрисовка растров с наложением внешнего альфа-канала, масштабированием, растяжением, искажением и поворотом растров. При этом можно установить режимы отображения отдельных пикселей - от простого переноса до префильтрации (наилучшее качество изображения). Стало возможным рисовать векторные примитивы, залитые текстурами (!).
  • Поддержка популярных форматов графических файлов: необычайно приятное новшество для всех программистов, имеющих дело с разными графическими форматами. Поддерживаются форматы BMP, GIF, TIFF, JPEG, Exif (расширение TIFF и JPEG для цифровых фотокамер), PNG, ICON, WMF и EMF. Декодеры различных форматов выполнены с учетом их специфики, так что Вы сможете, например, отобразить анимационный GIF или добавить комментарий к TIFF-файлу. Загруженный, созданный или модифицированный файл может быть сохранен на диск в одном из подходящих форматов. Существует возможность написания собственных декодеров.
  • Формат EMF+: разумеется, все это великолепие не могло уместиться в тесные рамки старого Enhanced Metafile. Для описания новых возможностей был создан новый формат метафайла EMF+, который позволяет сохранить на диск и затем проиграть последовательность графических команд. Существует возможность записать "дуальный" метафайл, понятный старым GDI-программам. Новые программы будут читать из него GDI+ - информацию.
Требования к среде выполнения

Поддержка GDI+ встроена непосредственно в операционные системы Windows XP и .NET Server. Для того чтобы приложения, использующие эту библиотеку, выполнялись на предыдущих версиях Windows, необходимо установить дистрибутив gdiplus_dnld.exe размером около одного мегабайта. Найти его (и, возможно, другие необходимые обновления) можно на сайте Microsoft по адресу:

http://www.microsoft.com/msdownload/platformsdk/sdkupdate/psdkredist.htm

В его состав входят только инструкция по установке и уже упомянутая динамическая библиотека GdiPlus.dll, которую необходимо скопировать в системный каталог Windows 98/ME, Windows NT SP6 или Windows 2000. При этом возможности, предоставляемые непосредственно ядром Windows XP (в частности, технология ClearType для качественного отображения шрифтов на LCD-мониторах), будут недоступны.

ПРИМЕЧАНИЕ
Я не случайно не упомянул про Windows 95. На сайте Microsoft отсутствует всяческое упоминание о поддержке GDI+ для этой операционной системы. Тем не менее, единственная доступная мне для тестирования машина с Windows 95 OSR2 выполнила тестовое приложение без каких-либо проблем. Но ввиду отсутствия какой-либо официальной поддержки для использования GDI+ крайне рекомендуется обновить систему хотя бы до Windows 98.
Поддерживаемые технологии разработки

В этой статье рассматривается интерфейс к GDI+, реализованный для языка C++ - хотя уже существует реализация Microsoft для системы CLR, входящей в состав .NET, и, безусловно, вскоре усилиями энтузиастов появятся другие (например, для VB и Delphi).

Заметим, что GDI+ (вернее, ее обертка для CLR), входящая в состав Microsoft .NET Framework SDK, является основным средством рисования в среде .NET. Однако доступная на данный момент Beta 2 имеет довольно большие отличия от реализации для C++ (не только архитектурные, но и чисто внешние, например, различающиеся имена некоторых классов). Я постараюсь коротко описать эти отличия в конце статьи.

Набор заголовочных файлов (headers) и библиотека импорта GdiPlus.lib, необходимые для сборки демонстрационных приложений, входят в состав последнего Platform SDK. Те, кто до сих пор не обновил идущий с Visual Studio 6.0 Platform SDK образца 1998 года, могут загрузить его с сайта Microsoft по адресу:

http://www.microsoft.com/msdownload/platformsdk/sdkupdate/

Минимальный компонент, в состав которого входит GDI+, называется Windows Core SDK и имеет размер около 230 мегабайт.

ПРИМЕЧАНИЕ
Я понимаю, что для многих читателей, имеющих доступ в Интернет через домашний модем, предложение скачать дистрибутив такого размера прозвучит как насмешка. В качестве крайней временной меры можно раздобыть только набор заголовочных файлов GdiPlus*.h, BaseTsd.h и библиотеку импорта GdiPlus.Lib из нового Platform SDK. Но гарантировать работоспособность такого решения во всех ситуациях я не возьмусь. Да и в любом случае, обновить Platform SDK необходимо. Возможно, вам удастся найти его на CD-ROM.

На момент написания этих строк доступна версия Platform SDK за август 2001 г.

Демонстрационные примеры будут в подавляющем большинстве написаны с использованием Windows API, что позволит сосредоточиться на использовании GDI+. Но вы без труда сможете подключить эту библиотеку к своим MFC- или WTL-приложениям. Иногда я также буду приводить соответствующий пример на C# для WinForms.

Начинаем работу
Иерархия классов GDI+

Типичное рабочее место программиста на C++, как правило, включает в себя стену, на которой гордо красуется Диаграмма классов (неважно каких). Теперь рядом можно наклеить еще один плакат.

Ниже приведена иерархия классов GDI+. Я не включил в нее 8 структур данных и перечисления (enumerations) - около 50 штук.

Иерархия классов GDI+

При первом взгляде на диаграмму видно, что она очень напоминает, например, ту часть библиотеки MFC, которая отвечает за рисование, только классов гораздо больше (40 против 15 у MFC). Это и неудивительно, учитывая фирму, которая разрабатывала эти библиотеки. Основные отличия отражают новые возможности GDI+. Мы подробно рассмотрим их в следующих частях.

Как видим, большинство объектов имеют в корне иерархии класс GdiPlusBase. Вам не понадобится создавать экземпляры этого класса, так как он содержит только средства управления памятью (для него перегружены операторы new/new[] и delete/delete[], которые используют функции GDI+ GdipAlloc и GdipFree). Все классы, инкапсулирующие работу с ресурсами GDI+, порождены от GdiPlusBase. Это не значит, что их экземпляры нельзя создавать на стеке - напротив, так даже удобнее контролировать время их жизни. Зато такая архитектура позволит, например, передавать указатель на созданный объект GDI+ в модуль, написанный с использованием других средств разработки, и безопасно его удалять в этом модуле.

ПРИМЕЧАНИЕ
Не путайте управление памятью под экземпляры классов-оберток С++, которое осуществляется перегруженными операторами new/delete, и управление собственно ресурсами GDI+, которое скрыто от разработчиков в недрах соответствующих функций, например, GdipCreateSolidFill.

Ключевым же классом в GDI+ является Graphics (программисты на J++ вздрогнули). Именно он содержит почти две сотни методов, отвечающих за рисование, отсечение и параметры устройства вывода. Напрашивается явная аналогия с контекстом устройства (Device Context) прежнего GDI, и эти понятия действительно тесно связаны. Из четырех конструкторов Graphics два создают его из HDC. Главное отличие заключается в изменении программной модели: теперь вы не работаете с хендлом, а вызываете методы класса. Хотя программистам на MFC эта концепция уже хорошо знакома.

Дальнейшее наследование (например, класс TextureBrush порожден от Brush) скорее отражает цели разработчиков (скрытие деталей реализации и повторное использование оберточного кода), чем инфраструктуру библиотеки, так как в inline-методах "родственных" классов просто содержатся вызовы различных функций GdiPlus.dll. Можно сказать, что Microsoft в очередной раз спроецировала обычный "плоский" API языка C на объектно-ориентированную библиотеку C++.

Оставшаяся часть классов не имеет общего родителя и предназначена для упрощения работы со структурами данных GDI+.

Инициализация и завершение

Перед тем как начать использовать классы и функции GDI+, необходимо инициализировать эту библиотеку. Для этого где-нибудь в начале своей программы нужно поместить вызов функции GdiplusStartup:

Status GdiplusStartup( ULONG_PTR* token,
const GdiplusStartupInput* input,
GdiplusStartupOutput* output );

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

"Магическое значение", на которое указывает выходной параметр token, необходимо сохранить.

Для завершения работы с библиотекой вызовите функцию GdiplusShutdown:

VOID GdiplusShutdown(
ULONG_PTR token
);

Здесь в качестве параметра и необходимо передать то самое число, которое возвратила GdiplusStartup в параметре token.

ПРИМЕЧАНИЕ
Вы можете вызвать GdiplusStartup и GdiplusShutdown из разных потоков, но необходимо убедиться, что вне этой пары функций никакого обращения к объектам GDI+ не происходит. В частности, будьте осторожны, объявляя глобальными экземпляры классов - ведь их деструкторы выполнятся уже после WinMain. Кроме того, как обычно, нельзя вызывать функции инициализации и очистки из DllMain, поскольку это может привести ко входу в бесконечную рекурсию или другим неприятностям.
Создаем первое приложение

Настало время применить все эти сведения на практике. Для этого создадим в MS Visual C++ базовое WINAPI-приложение, которое послужит полигоном для дальнейших экспериментов. Ниже для этого приведена пошаговая процедура.

Итак, создаем новый проект Win32 Application. Выбираем опцию A typical "Hello, World!" application и нажимаем "Finish". Получившееся приложение необходимо подготовить для использования GDI+. Для этого в файле stdafx.h после строки с комментарием:

// TODO: reference additional headers your program requires here

добавляем следующие строчки:

#include <GdiPlus.h>
using namespace Gdiplus;

и в конце файла stdafx.cpp добавляем строку

#pragma comment(lib, "GdiPlus.lib")

Кроме того, в файле stdafx.h необходимо удалить или закомментировать строку

#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers

Иначе компилятор выдаст кучу ошибок об отсутствии символов MIDL_INTERFACE, PROPID, IStream и т.д.

Если полученное в результате приложение успешно собралось, значит, мы все сделали правильно. Пойдем дальше.

Найдем в сгенерированном основном .cpp файле нашего проекта функцию WinMain и добавим в начале ее код инициализации:

GdiplusStartupInput gdiplusStartupInput;
ULONG_PTR gdiplusToken;
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);

а в конце, перед оператором return, добавим код очистки:

GdiplusShutdown(gdiplusToken);

Готово. Наконец-то мы можем что-нибудь нарисовать. Найдите в теле функции WndProc обработчик сообщения WM_PAINT и замените следующим кодом:

hdc = BeginPaint(hWnd, &ps);
OnPaint(hdc, ps.rcPaint);
EndPaint(hWnd, &ps);
return 0;

Теперь где-нибудь перед функцией WndProc создадим функцию OnPaint с кодом рисования:

void OnPaint(HDC hdc, const RECT& rc)
{
    // Все строки - в кодировке Unicode
    WCHAR welcome[]=L"Welcome, GDI+ !";

    // Создаем контекст рисования и устанавливаем
    // пиксельную систему координат
    Graphics g(hdc);
    g.SetPageUnit(UnitPixel);
    RectF bounds(0, 0, float(rc.right), float(rc.bottom));

    // Загружаем фоновое изображение и растягиваем его на все окно
    Image bg(L"BACKGRND.gif");
    g.DrawImage(&bg, bounds);

    // Создаем кисть с градиентом на все окно и полупрозрачностью
    LinearGradientBrush brush(bounds, Color(130, 255, 0, 0), Color(255, 0, 0, 255),
    LinearGradientModeBackwardDiagonal);

    // Готовим формат и параметры шрифта
    StringFormat format;
    format.SetAlignment(StringAlignmentCenter);
    format.SetLineAlignment(StringAlignmentCenter);
    Font font(L"Arial", 48, FontStyleBold);

    // Выводим текст приветствия, длина -1 означает,
    // что строка заканчивается нулем
    g.DrawString(welcome, -1, &font, bounds, &format, &brush);
}

В результате у нас получится примерно вот что:

ПРИМЕЧАНИЕ
Приведенный пример носит только ознакомительный характер. В реальном приложении, для того чтобы нарисовать растр, его, как правило, не нужно каждый раз загружать с дискового файла :). Далее я буду пользоваться созданным макетом программы для создания других демонстрационных приложений. В качестве примера рисования будет приводиться только код функции OnPaint.
Пример WinForms - приложения с использованием GDI+

Для того чтобы можно было сравнить рассматриваемую реализацию GDI+ с той, что используется в .NET, приведу полный текст соответствующего приложения на новом языке C#:

using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;

public class GraphicsForm: Form
{
  public static int Main()
  {
    Form fm = new GraphicsForm();
    fm.ShowDialog();
    return 0;
  }
  protected override void OnPaint(PaintEventArgs a)
  {
    DoPaint(a.Graphics, a.ClipRectangle);
  }

  protected void DoPaint(Graphics g, Rectangle clipBox)
  {
    RectangleF bounds = clipBox;
    string welcome = "Welcome, GDI+ !";
    Bitmap bg = new Bitmap("BACKGRND.gif");
    g.DrawImage(bg, bounds);
    LinearGradientBrush brush = new LinearGradientBrush(bounds,
    Color.FromArgb(130, 255, 0, 0), Color.FromArgb(255, 0, 0, 255),
    LinearGradientMode.BackwardDiagonal);
    StringFormat format = new StringFormat();
    format.Alignment = StringAlignment.Center;
    format.LineAlignment = StringAlignment.Center;
    Font font = new Font("Arial", 48, FontStyle.Bold);
    g.DrawString(welcome, font, brush, bounds, format);
  }
}

Как видим, помимо чисто синтаксических отличий имеются и принципиальные, например, использование в CLR-модели свойств против использования Set-методов в C++. Кроме того, в .NET активно используются пространства имен.

ПРИМЕЧАНИЕ
Замечу, что здесь приведен полный текст программы, аналогичной по возможностям той, что мы создали в предыдущем разделе. Сравните объем исходных текстов этих двух примеров. NO COMMENTS.

Если вы запустите приведенный пример, то увидите, что текст отрисовывается без сглаживания, характерного для предыдущего примера. Это связано с тем, что WinForms по умолчанию отключает улучшенный режим отрисовки шрифтов - и без этого причин для торможения достаточно :)

Несколько замечаний о компиляции и сборке проектов

Хочется указать на несколько "подводных камней", которые могут сбить с толку при первой попытке откомпилировать и собрать проект, использующий GDI+. В основном здесь упомянуты те проблемы, с которыми сталкиваются (и постоянно спрашивают о них в различных форумах) начинающие.

Где взять GdiPlus.h?

Как я уже сказал, все заголовочные файлы, библиотека импорта и документация к библиотеке входят в состав последнего Platform SDK. Они не идут в составе Visual С++ 6.0 и его сервис паков.

Почему выдается ошибка о типе ULONG_PTR?

Похоже, что компилятор находит старый заголовочный файл basetsd.h - например, из комплекта VC++. Измените пути поиска заголовочных файлов так, чтобы вначале были найдены файлы Platform SDK.

Почему компилятор не дает создать объект GDI+ при помощи new?

Такое поведение возможно при попытке откомпилировать MFC-приложение с использованием GDI+ в Debug-конфигурации.

В начале файла программы, видимо, имеется следующий фрагмент:

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

Либо откажитесь от создания объектов GDI+ с помощью new, либо откажитесь от проверок динамической памяти в этом файле (удалив вышеприведенную директиву #define).

Не забудьте про пространство имен Gdiplus и библиотеку импорта

В приводимых примерах кода используются простые имена классов, такие как Brush и Rect. Это стало возможным благодаря тому, что в начале заголовочного файла программы есть директива

using namespace Gdiplus;

Если это решение не подходит (например, в проекте уже существуют классы с такими именами), то перед именами классов необходимо ставить префикс пространства имен, например

Gdiplus::Rect rect;

Также, если по каким-то соображениям директива

#pragma comment(lib, "gdiplus.lib")

не устраивает, в опциях компоновщика нужно явно указать библиотеку импорта gdiplus.lib.

На этом пока все. В следующей части мы рассмотрим богатые возможности, которые GDI+ предоставляет для работы с растровыми изображениями.

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

Как вставлять в программу на C++ двоичные константы?
Автор: Александр Шаргин

В языке C++ есть восьмеричные, десятичные и шестнадцатеричные константы. А двоичных - нет. Тем не менее, при помощи препроцессора можно соорудить макрос, который позволит нам смоделировать такие константы. Основная идея - преобразовывать восьмеричную константу в двоичную, выделяя из неё отдельные цифры и умножая их на соответствующий весовой коэффициент. Есть только одна проблема: в тип long влезет не более десяти цифр, а этого хватит только на формирование двоичных констант длиной в байт. Хотелось бы иметь и более длинные двоичные константы. Чтобы решить эту проблему, можно ввести дополнительные макросы, которые будут склеивать короткие двоичные последовательности в более длинные. Эти макросы могут выглядеть примерно так.

#define BIN8(x) BIN___(0##x)
#define BIN___(x) \
( \
((x / 01ul) % 010)*(2>>1) + \
((x / 010ul) % 010)*(2<<0) + \
((x / 0100ul) % 010)*(2<<1) + \
((x / 01000ul) % 010)*(2<<2) + \
((x / 010000ul) % 010)*(2<<3) + \
((x / 0100000ul) % 010)*(2<<4) + \
((x / 01000000ul) % 010)*(2<<5) + \
((x / 010000000ul) % 010)*(2<<6) \
)

#define BIN16(x1,x2) \
((BIN(x1)<<8)+BIN(x2))

#define BIN24(x1,x2,x3) \
((BIN(x1)<<16)+(BIN(x2)<<8)+BIN(x3))

#define BIN32(x1,x2,x3,x4) \
((BIN(x1)<<24)+(BIN(x2)<<16)+(BIN(x3)<<8)+BIN(x4))

Для компиляторов, поддерживающих 64-разрядные целые, можно также ввести дополнительный макрос BIN64. Для Visual C++ он будет выглядеть так.

#define BIN64(x1,x2,x3,x4,x5,x6,x7,x8) \
((__int64(BIN32(x1,x2,x3,x4)) << 32) + __int64(BIN32(x5,x6,x7,x8)))

Обратите внимание, что к параметру макроса BIN8 при помощи оператора ## принудительно дописывается ведущий ноль, чтобы с ним можно было работать как с восьмеричной константой. Благодаря этому пользователь может смело применять макрос BIN8 как к числу с ведущими нулями, так и без них, и всё будет работать именно так, как он ожидает.

Использовать наши макросы можно примерно так.

char i1 = BIN8 (101010);
short i2 = BIN16(10110110,11101110);
long i3 = BIN24(10110111,00010111,01000110);
long i4 = BIN32(11101101,01101000,01010010,10111100);

Самое замечательное состоит в том, что, хотя выражения для пересчёта из десятичной системы в двоичную получаются очень громоздкими, они остаются константными, а значит будут вычисляться в процессе компиляции. Это, в частности, означает, что наши двоичные числа можно использовать везде, где требуется константа (для задания размера массива, в метках case оператора switch, для указания ширины битового поля и т. д.).

Реализация макроса BIN8, показанная выше, достаточно прямолинейна. Этот же макрос можно реализовать более элегантными способами. Например, вот вариант, предложенный Игорем Ширкалиным.

#define BIN__N(x) (x) | x>>3 | x>>6 | x>>9
#define BIN__B(x) (x) & 0xf | (x)>>12 & 0xf0

#define BIN8(v) (BIN__B(BIN__N(0x##v)))

До встречи через неделю!

Алекс Jenter jenter@rsdn.ru
Duisburg, 2001. Публикуемые в рассылке материалы принадлежат сайту RSDN.


ПРОГРАММИРОВАНИЕ НА V I S U A L C + +
РАССЫЛКА САЙТА
RSDN.RU
Выпуск No. 58 от 30 декабря 2001 г.

РАССЫЛКА ЯВЛЯЕТСЯ ЧАСТЬЮ ПРОЕКТА RSDN , НА САЙТЕ КОТОРОГО ВСЕГДА МОЖНО НАЙТИ ВСЮ НЕОБХОДИМУЮ РАЗРАБОТЧИКУ ИНФОРМАЦИЮ, СТАТЬИ, ФОРУМЫ, РЕСУРСЫ, ПОЛНЫЙ АРХИВ ПРЕДЫДУЩИХ ВЫПУСКОВ РАССЫЛКИ И МНОГОЕ ДРУГОЕ.

Здравствуйте, дорогие друзья!

Сердечно поздравляю вас всех с наступающим Новым Годом! Пусть он принесет вам как можно больше успехов и радости, откроет новые горизонты и пусть лучезарное настроение никогда вас не покидает! Оно ведь вам особенно необходимо, когда вы например отслеживаете сорок шестую ошибку в программе... ;-)

Но одним поздравлением я сегодня все-таки не ограничусь :-) Cначала хочу внести ясность: предыдущему выпуску номер "58" был присвоен случайно и вне очереди, т.к. на самом деле он только пятьдесят седьмой. В архиве он за этим номером и стоит, так что просьба не искать потерявшийся 57-ой выпуск. ;-)

Cегодня я хочу представить вашему вниманию очень интересную, на мой взгляд, статью Владислава Чистякова из журнала "Технология Клиент-Сервер". Статья посвящена перспективам, ждущих нас уже в совсем недалеком будущем. Как вы вероятно уже знаете, входящая в платформу .NET Common Language Runtime (CLR) призвана поднять переносимость создаваемых приложений на новый уровень. Но давайте не будем забегать вперед...

CТАТЬЯ

CLR
Common Language Runtime
Автор: Владислав Чистяков
Источник: <Технология Клиент-Сервер>

Прежде чем начинать говорить про VS.Net, необходимо поговорить про .Net и про рекламу в общем. Что же такое .Net и зачем он нужен?

Вы, наверное, заметили, что чем больше Интернет проникает в массы, тем больше нечестного использования этого названия встречается. Например: "Новый процессор Pentium 4 позволит поднять на НОВЫЙ уровень ваши возможности в Интернет". Интересно, ведь если даже у счастливого обладателя этого процессора будет возможность смотреть видеоролик в режиме 1900x1600 (хотя, хоть убей не пойму, как это зависит от процессора?), то где он возьмет канал в Интернет, который даст ему возможность прокачать этот ролик (ну, хотя бы с приемлемым качеством в разрешении 320x240). Но слово магическое - ИНТЕРНЕТ! Всунул его в свой пресс-релиз и порядок, продажи обеспечены.

Никуда не делся от этого искушения и Microsoft. Все без исключения продукты этого производителя будут теперь иметь суффикс - <.Net>.

Неважно, на сколько процентов продукт предназначен для Интернет. Можно сказать больше - в марку .Net вкладывается столько денег, что в один прекрасный день, чтобы объяснить молодому специалисту, что такое Интернет, ему скажут: "Интернет - это инфраструктура, предназначенная для запуска приложений и сервисов .Net". Вы думаете, я утрирую или шучу. Нисколько. Буквально за день, до того как сесть писать эту статью я слышал как ведущий радиостанции, по-моему, РДВ, заявил: "как хорошо все-таки, что Билл Гейтс придумал Интернет, а то я бы не смог получать ваши электронные письма...".

Ну да ладно. Пускай специалисты из Microsoft отдуваются за маркетинговые изыски своего начальства, объясняя: что же такое .Net? Наша задача разобраться - что же такое VS.Net?

Можно сказать, что VS.Net - это всего лишь новая версия VS - седьмая версия, но это не совсем так. Дело в том, что практически все составные части VS были полностью разрушены и выстроены заново. Короче говоря, Microsoft в очередной раз воплотил в жизнь принцип: МЫ НАШ, МЫ НОВЫЙ МИР ПОСТРОИМ... Но в отличие от прошлых разов, когда Microsoft в целях строительства нового (своего нового) мира разрушал миры своих оппонентов, в этот раз Microsoft, на первый взгляд, разрушил свой, причем уютненький такой мирок. Так что предпосылка "кто был ничем" не срабатывает ;o).

Что это, агония?

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

Стратегия "пан или пропал"

Недавно обратил внимание на незакрытое окно броузера, в котором была открыта статья с русскоязычного сайта Sun Microsystems под названием , аж в четырех частях. В ней планомерно доказывалась, что UNIX (и особенно Linux) лучше, чем NT, по всем показателям (одно только осталось непонятным, а если все так, как говорится в этой статье, почему весь мир не сбежал на UNIX с этой никчемной NT). Практически вся статья построена на демагогии, и она даже не заслуживала бы упоминания, если бы ни одно <но>. В этой статье давался подробный список недочетов, допущенных Microsoft в NT 4. Забавно, но это именно те недочеты, которые были устранены в 5-й версии (Windows 2000). Из всего упомянутого, по-моему, только отсутствие в поставке серьезного mail-сервера осталось не исправленным.

Та же история и со вторым конкурентом - с Oracle. Его маркетинг во многом строился на критике Microsoft SQL Server. Временами казалось, что специалисты из Oracle попросту подрабатывают бета-тестерами у Microsoft. И что в итоге? В первую очередь в SQL Server 2000 были внесены те исправления и замечания, о которых говорил Oracle.

Причем чем больнее задевают Microsoft, тем больше вероятность, что следующая версия продукта выбьет колючие аргументы из рук оппонентов.

Та же ситуация складывается и со средствами разработки. Похоже, Microsoft внял критике, звучавшей со всех сторон, и решил разрушить все созданное им за весь период существования. И делается это отнюдь не из мазохистских побуждений. Просто Microsoft хочет одним махом подчистить весь "баг-лист", любезно предоставляемый конкурентами и прочими доброжелателями. Складывается впечатление, что менеджеры проектов в Microsoft не знают одну из поговорок программистов: "старый баг лучше новых двух". Хотя перед выходом SQL Server, сначала 7.0, а потом и 2000, злые языки болтали, что ввиду больших переделок ядра SQL Server окажется глючным, вследствие чего непригодным для решения ответственных задач, а вышло все наоборот. Но тогда изменения были все-таки не такими глобальными, да и бета 1 была уже полностью работоспособной. Бета 1 VS.Net же работоспособной назвать можно, но глюков в ней предостаточно.

Ну да ладно. И что ж за баг-лист такой, что необходимо все создавать заново?

  1. Отсутствие собственного реально переносимого между платформами стандарта (типа Java).

  2. Слабая интеграция имеющихся (ну, может быть, за исключением InterDev) средств разработки с Интернетом.

  3. Сложность, неоднозначность и другие недостатки имеющейся компонентной модели (COM).

  4. Разный уровень интеграции средств разработки с COM и "напряженность между базовыми концепциями языков программирования и COM-ом".

  5. Увеличивающаяся популярность Java в ущерб популярности VB, фаворита от Microsoft.

Нет никакой ошибки в том, что в список недостатков два раза попали упоминания про Java. Политические и экономические аспекты в современном мире всегда главенствовали над технократическими.

Что же придумал Microsoft для решения этих, а заодно и других, более мелких проблем? Microsoft придумал очень много рекламных терминов, главным по частоте звучания, несомненно, является .Net, но действительно главным, можно сказать, поворотным является CLR и основанный на нем .NET Framework.

.NET Framework - это среда для создания, распространения и исполнения как обычных, так и Web-приложений. Она состоит из двух частей - Common Language Runtime и Framework-классов. В VS.Net Web- приложения получили особый статус. Теперь можно как создавать ASP-приложения, так и использовать новую идеологию Web-сервисов. Все эти новаторства объединены под общим названием ASP.NET, и подразумевают, что для создания приложений будут использоваться CLR-совместимые языки. Однако Web-приложения можно создавать и на старом добром C++. Для этого в VS.Net был добавлен новый ATL-шаблон - ATL Sever. Это шаблон, позволяющий создавать приложения а-ля ASP, но на C++. Доступ к Интернет-серверу осуществляется через специально созданный для ATL Sever ISAPI-фильтр. Собственно ASP.Net - это, грубо говоря, тоже ISAPI- фильтр, но с большей рекламой.

.NET Framework позволяет создавать замечательные web-приложения. Но он применим и для создания обычных десктоп- приложений. Если вы пишете любое ПО для Windows (используя ATL/COM, MFC, Visual Basic или просто стандартное Win32), вы найдете в .NET немало достоинств.

Для улучшения взаимодействия между языками в Microsoft .NET Framework введён языковый стандарт, Common Language Specification (CLS). CLS - это поднабор свойств языка, поддерживаемых CLR, и включающий свойства, общие для большинства объектно-ориентированных языков программирования. Если вы хотите, чтобы ваши компоненты и элементы управления можно было использовать из других языков программирования, их нужно создавать на CLR-совместимом языке, и обеспечить совместимость всех общих и частных членов с CLR.

Языки, поддерживаемые VS.Net

Изначально Microsoft включает в поставку VS.Net компиляторы для C#, Visual Basic, Managed C++ (MC++) и JScript. Сторонние разработчики уже создали .NET-компиляторы для других языков, включая: Java (Rational), Eiffel (Interactive Software Engineering and Monash University), Perl (ActiveState), Python (ActiveState), Scheme (Northwestern University), Smalltalk (Quasar Knowledge Systems), Cobol (Fujitsu), Component Pascal (Queensland University of Technology), APL (Dyalog), Standard ML (Microsoft Research- Cambridge), Mercury (University of Melbourne) и Oberon (ETH Zentrum). В спорах между COM и CORBA приводились аргументы о количестве применимых языков, но ни та, ни другая технология и близко не подходили к списку такой длины. А ведь это только начало!

Мало того, с помощью входящих в поставку библиотек можно даже создать свой CLR-совместимый язык программирования, который будет генерировать исполняемые модули. В качестве примеров поставляются три прототипа языков: smc (настоящий компилятор Managed C++, несколько упрощенный, но все же), MyC (ограниченная реализация языка C), и CLisp (ограниченная реализация языка Lisp). Интересно, что smc - это C++-проект, который компилируется на VC 6. В его readmе сказано, что в релиз-версии он будет компилироваться как на нормальном компиляторе C++, так и на самом себе! Есть пример отладчика командной строки и профайлера. Судя по всему, нас ждет увлекательный год!

Что же такое CLR?

CLR расшифровывается как "Common Language Runtime" (межъязыковый рантайм). Чтобы понять, зачем он нужен, необходимо предварительно проанализировать текущее состояние дел в COM и Java- технологиях.

Для начала приведем определение из материала <Microsoft .Net Common Language Runtime Architecture", базовой спецификации, поставляемой Microsoft.

<...Common Language Runtime управляет исполнением исходного кода после его компиляции в Microsoft Intermediate Language(MSIL), OptIL или машинные коды.Весь код на MSIL или OptIL исполняется как управляемый код (managed code); этот код исполняется в сотрудничестве с .Net Framework. .Net Framework обеспечивает предоставляет управление памятью, кросс- языковую интеграцию, обработку исключений, защиту кода и автоматическое управление сроком жизни объектов. В свою очередь, управляемый код должен предоставить в метаданных информацию, достаточную, чтобы позволить .Net Framework управлять исполнением кода.

Ключевым свойством CLR является возможность обеспечения программной изоляции приложений, исполняемых в общем адресном пространстве. Это осуществляется с помощью типо-безопасного доступа ко всем областям памяти при исполнении типо-безопасного управляемого кода. Некоторые компиляторы могут создавать MSIL-код, который не только типо-безопасен, но и поддается простой проверке на безопасность исполнения. Этот процесс называется верификацией и позволет серверам просто проверять написанные на MSIL пользовательские программы, и запускать только те, которые не будут производить небезопасных обращений к памяти. Такая независимая верификация важна для действительно масштабируемых серверов, исполняющих пользовательские программы и скрипты.>

MSIL - это некоторый язык инструкций, похожий на независимый от платформы ассемблер. Внутри CLR-совместимого исполняемого модуля помещается некоторый p-код, состоящий из MSIL-инструкций. Но с помощью утилиты ildasm из p-кода можно получить текстовое представление MSIL Оно выглядит примерно так:

/* Displays the error string according to the passed in error code.
     Error code must be one of the values declared by the enumeration. */
  .method virtual newslot famorassem hidebysig instance
     void ShowErrorText(value class ErrorCodes errorCode)
     synchronized il managed {
    // load the appropriate error string
    ldarg.0
    ldfld  class
         [.module CountDownErrorLabel.dll]ErrorLabel CountDownForm::errorLabel
    ldarg.0
    ldfld  class System.String[] CountDownForm::errorStrings
    ldarg  errorCode  // load the value of the enumeration
    ldelem.ref
    // error string on stack, display
    callvirt instance void class
     [.module CountDownErrorLabel.dll]ErrorLabel::set_Text(class System.String)
    ret
  }

Такой код можно скомпилировать обратно в исполняемый файл с помощью утилиты ilasm. Это позволяет, например, при программировании на языке, не поддерживающем полностью всех возможностей CLR, скомпилироваться во MSIL, дизассемблировать его и добавить недостающие элементы вручную. Такие широкие возможности дизассемблирования очень порадовали бы хакеров, но Microsoft предусмотрел средства, позволяющие предотвратить дизассемблирование готового модуля. К сожалению, в beta 1 это можно сделать только с помощью повторной компиляции дизассемблированного кода из командной строки. В будущем эта опция будет встроена непосредственно в компилятор. К выходу VS.Net Microsoft обещает сделать так, чтобы приложения компилировались непосредственно при инсталляции, или даже при создании инсталляторов для конкретных платформ. Пока же компиляция в машинный код происходит только при загрузке программы.

Ограничения информации о типах в COM и языках программирования

Одно из преимуществ, дарованных нам COM - динамическая загрузка компонентов. Причем загрузка экземпляров конкретных компонентов осуществляется на базе типов. Когда код загружен, программисты разрешают точки входа привязкой объектных ссылок к новым абстрактным типам. Для облегчения первого COM предоставляет CoCreateInstance как типо-ориентированную альтернативу файл-ориентированному вызову API LoadLibrary. Для облегчения последнего COM предоставляет метод QueryInterface как типо-ориентированную альтернативу символьно-ориентированному вызову API GetProcAddress. Посмотрите на следующий COM/C++ код:

IAntique *pAntique = 0;
HRESULT hr = CoCreateInstance(CLSID_Pinto, 0,
    CLSCTX_ALL, IID_IAntique, (void**)&pAntique);
if(SUCCEEDED(hr))
{
  ICar *pCar = 0;
  hr = pAntique->QueryInterface(IID_ICar, (void**)&pCar);
  if(SUCCEEDED(hr))
  {
    hr = pCar->AvoidFuelTankCollision();
    if(SUCCEEDED(hr))
    {
      hr = pAntique->Appreciate();
    }
    pCar->Release();
  }
  pAntique->Release();
}

Заметьте, что нигде нет вызовов LoadLibrary или GetProcAddress. Этот код избавлен от подробностей типа физического размещения DLL- библиотеки (мы даже не знаем, в DLL или в EXE хранится код компонента) или явного запроса адреса метода по символическому имени с последующим преобразованием адреса в указатель на функцию. Но этот код неуклюж и велик по сравнению с кодом создания экземпляра C++-класса и его приведения к базовому классу:

СPinto Antique;
CCar & Car = (CCar)Antique
Car.AvoidFuelTankCollision();
Antique. Appreciate();

В чем же разница между этими листингами? В первом из них на языке программирования C++ был динамически создан экземпляр компонента, возможно, созданного на другом языке и располагающегося в отдельном исполняемом модуле, а во втором был создан экземпляр класса, определенного в той же программе (а значит, написанного на том же языке, располагающегося в том же модуле...). В остальном же эти листинги идентичны.

Ключ к пониманию недостатков COM спрятан именно в первом листинге. Этот код иллюстрирует напряженность между системой типов COM и системой типов языка реализации (в данном случае, C++). Заметьте, что везде, где объектная ссылка возвращается вызывающей стороне, ее должен сопровождать GUID (в этом примере IID_IAntique или IID_ICar). Это потому, что идентификатор типа языка реализации (std::type_info в случае C++) несовместим с форматом идентификатора типа COM.

За долгие годы группа C++ в Microsoft представила несколько технологий, позволяющих сгладить разницу между системами типов C++ и COM, самой важной (хотя и хитрой) из которых были расширения языка: __uuidof и __declspec(uuid). Эти расширения позволили ассоциировать GUID (или, как его еще называют, UUID) с некоторым пользовательским типом. Компилятор MIDL при обработке IDL-файлов автоматически ассоциирует идентификатор типа (GUID) COM с символическим именем С++-типа. При использовании __uuidof код становится более типобезопасным:

IAntique *pAntique = 0;
HRESULT hr = CoCreateInstance(__uuidof(Pinto), 0,
    CLSCTX_ALL, __uuidof(pAntique), (void**)&pAntique);
if(SUCCEEDED(hr))
{
  ICar *pCar = 0;
  hr = pAntique->QueryInterface(__uuidof(pCar), (void**)&pCar);
  if(SUCCEEDED(hr))
  {
    hr = pCar->AvoidFuelTankCollision();
    if(SUCCEEDED(hr))
      hr = pAntique->Appreciate();
    pCar->Release();
  }
  pAntique->Release();
}

Заметьте, что, если понадобится изменить тип pAntique, в первом листинге придется менять и IID, а во втором все произойдет автоматически, так как оператор __uuidof всегда получает нужный IID отталкиваясь от pAntique.

Более того, если воспользоваться возможностями самого C++, можно создать универсальные классы-обертки, еще более упрощающие жизнь программиста. Так при использовании CComPtr или поддержки COM компилятором (compiler COM support) можно написать примерно такой код:

CComPtr<IAntique> spIAntique;
HRESULT hr = spIAntique.CoCreateInstance(__uuidof(Pinto));
if(SUCCEEDED(hr))
{
  CComQIPtr<ICar> spICar(spIAntique);
  if(spIUnknown)
    hr = spICar->AvoidFuelTankCollision();
      if(SUCCEEDED(hr))
        hr = spIAntique->Appreciate();
}

Хотя этот код, несомненно, проще и безопаснее, он все же далек от идеала, причем как с точки зрения простоты и читабельности, так и с точки зрения типобезопасности. Ведь только во время исполнения программы будет точно известно, реализует объект эти интерфейсы или нет. Несмотря на использование __uuidof, чтобы заставить COM работать с С++, нужно отключить систему проверки типов С++ на время трансляции объектных ссылок COM в С++-типы. В отличие от этого, интеграция COM с виртуальной машиной Java Microsoft позволяет написать следующее:

IAntique antique = new Pinto();
ICar car = (ICar)antique;
car.AvoidFuelTankCollision();
antique.Appreciate();

Заметьте, что этот код наиболее близок к чистому коду на C++, разве что объект создается динамически, но компоненты и нельзя создавать на стеке.

Это пример, как и самый первый, загружает компонент на основании его типа, а не имени файла. Оба примера разрешают точки входа в компонент, используя средства приведения типов, а не символьные точки входа. Первое различие - Microsoft VM for Java выполняет огромную работу по состыковке системы типов Java с системой типов COM, и программистам не приходится делать этого вручную. Это и есть движущая сила для новой платформы - обеспечить универсальную среду исполнения компонентов, которой сможет воспользоваться любой компилятор, средство или служба. Новая среда - это и есть CLR!

А теперь взгляните на то, как выглядел аналогичный код в VB 6:

Dim thePinto as new Pinto, antique as IAntique
Set antique = thePinto
Dim car as ICar
Set car = antique
car.AvoidFuelTankCollision
antique.Appreciate

Сравните этот код с Java-кодом. На первый взгляд они идентичны, но не спешите делать окончательные выводы. Заметьте, что оператор Set в VB 6 кардинально отличается от приведения типов в Java. По сути Set аналогичен вызову QueryInterface (QI). Результат выполнения QI будет известен только на этапе выполнения, а приведение типа к базовому классу (интерфейсу) можно проверить еще на стадии компиляции.

Типобезопасность - это еще один постулат CLR.

Итак, CLR - это рантайм-среда, призванная упростить и обезопасить работу с компонентами для любого совместимого с ней средства или языка. Замечательно, скажете вы, но зачем же было ломать все и вся? Не лучше ли было подправить спецификацию COM, уточнить требования, предъявляемые к языкам программирования, ведь, например, VB совсем чуть-чуть не удовлетворяет этим требованиям? Это была бы, хотя и бурная, но эволюция. А так снова революция с неизбежным разрушением всего старого и с еще более неизбежной "наклепкой" всего нового. Причем это новое - не маленькая фитюлька, а то самое ВСЁ. На этот вопрос можно ответить, если задаться вопросом: а что, собственно, надо Microsoft? Постепенно улучшая свое программное обеспечение, с каждой новой версией доводить его до совершенства, тем самым поддерживать своих пользователей и зарабатывать уважение. Уважение?! А зачем оно Microsoft? Естественно, Microsoft жаждет не уважения, а господства, полного захвата рынка. Причем эти планы должны выполняться в среде открытой, практически честной конкуренции, где Microsoft может полагаться только на деньги, решительность и напористость.

При таком раскладе эволюция смерти подобна. Надо ломать при малейшем подозрении на несоответствие высоким идеалам, и строить заново. А уж если строить, то, конечно же, новый мир.

Есть и другое объяснения того, почему Microsoft так решительно отвергла все свои наработки и взялась за новую лучшую концепцию. Лучше всего эту версию изложил Ron Burk из WDJ. Вот его версия:

История программных революций от Microsoft, вкратце: Сначала были Windows API и DLL Hell. Революцией ?1 было DDE - помните, как ссылки позволили нам создавать статусные строки, отражающие текущую цену акций Microsoft? Примерно тогда же Microsoft создала ресурс VERSION INFO, исключающий DLL Hell. Но другая группа в Microsoft нашла в DDE фатальный недостаток - его писали не они!

Для решения этой проблемы они создали OLE (похожее на DDE, но другое), и я наивно вспоминаю докладчика на Microsoft-овской конференции, говорящего, что скоро Windows API перепишут как OLE API, и каждый элемент на экране будет ОСХ-ом. В OLE появились интерфейсы, исключающие DLL Hell. Помните болезнь с названием "по месту", при которой мы мечтали встроить все свои приложения в один (возможно, очень большой) документ Word? Где-то в то же время Microsoft уверовала в религию С++, возникла MFC решившая все наши проблемы еще раз.

Но OLE не собиралась, сложа руки смотреть на это, поэтому оно заново родилось под именем COM, и мы внезапно поняли, что OLE (или это было DDE?) будет всегда - и даже включает тщательно разработанную систему версий компонентов, исключающую DLL Hell. В это время группа отступников внутри Microsoft обнаружила в MFC фатальный недостаток - его писали не они! Они немедленно исправили этот недочет, создав ATL, который как MFC, но другой, и попытались спрятать все замечательные вещи, которым так упорно старалась обучить нас группа COM. Это заставило группу COM (или это было OLE?) переименоваться в ActiveX и выпустить около тонны новых интерфейсов (включая интерфейсы контроля версий, исключающие DLL Hell), а заодно возможность сделать весь код загружаемым через броузеры, прямо вместе с определяемыми пользователем вирусами (назло этим гадам из ATL!).

Группа операционных систем громким криком, как забытый средний ребенок, потребовала внимания, сказав, что нам следует готовиться к Cairo, некой таинственной хреновине, которую никогда не могли даже толком описать, не то, что выпустить. К их чести, следует сказать, что они не представляли концепции "System File Protection", исключающей DLL Hell. Но тут некая группа в Microsoft нашла фатальный недостаток в Java - её писали не они! Это было исправлено созданием то ли J, то ли Jole, а может, и ActiveJ (если честно, я просто не помню), точно такого же как Java, но другого. Это было круто, но Sun засудило Microsoft по какому-то дряхлому закону. Это была явная попытка задушить право Microsoft выпускать такие же продукты, как у других, но другие.

Помните менеджера по J/Jole/ActiveJ, стучащего по столу туфлей и говорящего, что Microsoft никогда не бросит этот продукт? Глупец! Все это означало только одно - недостаток внимания к группе ActiveX (или это был COM?). Эта невероятно жизнерадостная толпа вернулась с COM+ и MTS наперевес (может, это стоило назвать ActiveX+?). Непонятно почему к MTS не приставили COM или Active или X или + - они меня просто потрясли этим! Они также грозились добавить + ко всем модным тогда выражениям. Примерно тогда же кое-кто начал вопить про Windows DNA (почему не DINA) и "Windows Washboard", и вопил некоторое время, но все это почило раньше, чем все поняли, что это было.

К этому моменту Microsoft уже несколько лет с нарастающей тревогой наблюдала за интернет. Недавно они пришли к пониманию, что у Интернет есть фатальный недостаток: ну, вы поняли. И это приводит нас к текущему моменту и технологии .NET (произносится как "doughnut" (пончик по-нашему), но по-другому), похожей на Интернет, но с большим количеством пресс-релизов. Главное, что нужно очень четко понимать - .NET исключает DLL Hell.

В .NET входит новый язык, C#, (выясняется, что в Active++ Jspresso был фатальный недостаток, от которого он и помер). .NET включает виртуальную машину, которую будут использовать все языки (видимо, из-за фатальных недостатков в процессорах Интел). .NET включает единую систему защиты (есть все-таки фатальный недостаток в хранении паролей не на серверах Microsoft). Реально проще перечислить вещи, которых .NET не включает. .NET наверняка революционно изменит Windows-программирование... примерно на год.

Еще одну версию причин ломки старого можно прочесть в статье про C#.

Ну да ладно, давайте попробуем разобраться, чем же так хороша новая концепция, что из-за нее бросаются все прошлые наработки.

Новаторства CLR

CLR - новая реализация идей, впервые в общедоступной форме изложенных в модели программирования COM. В CLR программисты загружают компоненты по типу, а не имени файла; разрешают точки входа, используя операции приведения типов, а не символьные точки входа. Модель программирования CLR в фундаментальном отношении не отличается от модели программирования COM. Итак, если модель программирования CLR так похожа модель программирования COM, зачем она вообще нужна? Ответ лежит в реализации.

По мнению группы разработчиков CLR из Microsoft, корень всех проблем кроется в эволюции информации о типах COM. Первые COM- интерфейсы вообще не имели универсального описания. Потом ввели IDL. Он был предназначен для генерации proxy/stub-DLL, которая, в свою очередь, использовалась dk вызовов методов интерфейсов между процессами или удаленными компьютерами.

Параллельно развивалась ветка VB, в которой появились бинарные описания компонентов под названием OLB (Object Library). Впоследствии OLB превратились в TLB (Type Library) но сути своей от этого не поменяли. Сначала в TLB-описаниях можно было использовать только очень ограниченный набор подтипов, что не давало применять их как единый стандарт описания компонентов. Так что существовало три вида описания типов: IDL, библиотеки типов и генерируемые MIDL /Oicf-строки, вставляемые внутрь proxy/stub-DLL. Ни один из этих трех форматов не стал абсолютным стандартом, и в одном формате можно записать информацию, которую невозможно представить в двух других. Это усложняло жизнь как разработчикам инфраструктуры COM, так и обычным программистам, создающим приложения из компонентов. Со временем библиотеки типов стали стандартом де-факто, но возможность обходиться без них, а иногда и вообще обходиться без описания типов, создавала некоторую неопределенность. Так, описание большинства низкоуровневых интерфейсов доступно только в виде заголовочных файлов C++, и, значит, недоступно для разработчиков, использующих более высокоуровневые языки типа VB или Java (например, OLE 2 API, или OLE DB API).

К тому же библиотеки типов COM описывают только типы, экспортированные из компонента. Информация об экспортированных типах позволяет средствам, работающим с COM, реализовать возможности наподобие IntelliSense в Visual Basic или декларативной архитектуры сервисов COM+. Это замечательно, но, по мнению господ из Microsoft, этого недостаточно. В COM не было возможности определить, от каких внешних компонентов зависит компонент. В то время как COM здорово поработал, чтобы сделать dumpbin.exe /exports устаревшим, он ничего не сделал, чтобы заменить dumpbin.exe /imports. Отсутствие информации о зависимостях затрудняет верное определение состава DLL (и их версий), необходимых для нормальной работы компонента.

Вот если бы хранить в библиотеке типов не только информацию об экспортированных вовне типах, а еще и обо всех (в том числе внешних) остальных типах, используемых в библиотеке... Тогда можно было бы точно определять список компонентов, а через них и библиотек, от которых зависит наша библиотека. К тому же это сделало бы возможным создание ряда сервисов, например, автоматической сериализации, отслеживания графа объекта, или автоматического принятия пре- и пост-условий. Большинство разработчиков, использующих COM в своей работе, давно хотели иметь полную информацию о типах. Все эти возможности заложены в CLR.

Главное достоинство CLR - всепроникающая, расширяемая, качественная информация о типах. В CLR все типы - а не только экспортированные из компонента - имеют runtime-описания типов. В CLR вы можете перейти к любому объекту и исследовать каждый аспект его определения типов, включая его представление. Это фундаментальный отход от классического COM, где открытые (public) интерфейсы были доступны, но представление объекта было скрыто.

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

Попробуем подытожить вышесказанное, перечислив усовершенствования, внесённые CLR в компонентную модель, попутно сравнивая эти усовершенствования с уже имеющимися в COM и других компонентных и околокомпонентных архитектурах.

Сборки. Сборки, или в оригинале Assembly - это логическая коллекция типов, описывающая объекты, которые могут быть реализованы в нескольких модулях (DLL или EXE). Сборка определяет область имен (типов), снимая надобность в GUID-ах для каждого типа. Так как имена сборок не могут повторяться, коллизии имен типов случиться также не может.

Для компонентов, используемых в рамках одного приложения, имена файлов сборки достаточно уникальны. CLR-сборки, в которых содержатся разделяемые компоненты (используемые совместно несколькими приложениями), можно присвоить <строгие имена> (strong names). Строго именованная сборка имеет 128-битный публичный ключ, который идентифицирует разработчика компонента. Когда программа-клиент связывается со строго именованной сборкой, 64- битный хеш публичного ключа сохраняется в метаданных этой программы. Во время исполнения публичный ключ сборки сравнивается с 64-битным хешем, хранящимся в метаданных клиента, обеспечивая загрузку нужной сборки.

В COM для обеспечения уникальности почти каждому типу соответствует 128-битный GUID. В CLR каждая сборка имеет 128- битный публичный ключ, что, в сочетании с локально уникальными символьными именами типов, обеспечивает глобальную уникальность описания типов. Оба способа дают примерно одинаковый эффект, но способ CLR позволяет избежать работы с GUID- ами внутри приложений.

Единый стандарт обмена метаданными. Как уже говорилось раньше, информация о типах в COM передавалась в текстовой (IDL, заголовочные файлы) или в бинарной (TLB) форме. В CLR, напротив, информация о типах всегда передается в одной и той же документированной бинарной форме. Все работающие с CLR средства и компиляторы выдают и принимают метаданные в этом формате. Так, при определении набора интерфейсов разработчик может использовать свой любимый язык программирования и компилятор для создания описаний типов, вместо того, чтобы использовать один синтаксис (IDL) при описании типов и другой (например, C++ или Visual Basic) при их реализации.

Доступность метаданных во время исполнения. Из постулата единого формата метаданных вытекает доступность метаданных в runtime, даже если во время разработки метаданные доступны не были. Причем можно не только читать, но и писать метаданные (создавать новое описание). Это дает возможность создавать динамически расширяемые приложения, позволяющие использовать информацию о типах для подключения внешних модулей или динамического вызова методов и установки свойств. Одним словом предполагается, что такие сложные приложения, как контейнеры объектов (дизайнеры форм, менеджеры транзакций и т.п.) можно будет писать на любом языке программирования, даже на VB. Более того, описание, сделанное на одном языке, можно будет использовать в другом без каких либо дополнительных действий. Еще более того, можно будет наследовать классы одного языка от классов, описанных на другом языке.

Например, рассмотрим следующий COM IDL:

[ uuid(ABBAABBA-ABBA-ABBA-ABBA-ABBAABBAABBA) ]
library MyLib
{
  importlib("stdole32.tlb");
  [ uuid(87653090-D0D0-D0D0-D0D0-18121962FADE) ]
  interface ICalculator : IUnknown
  {
    HRESULT Add([in] double n,
                [in, out] VARIANT_BOOL *round,
                [out] VARIANT_BOOL *overflow,
                [out, retval] double *sum);
  }
}

Эквивалентный тип CLR на С# (это новый язык программирования, который претендует стать основным языком VS.Net, о нем мы еще подробно поговорим позже) будет выглядеть так:

namespace MyLib
{
  interface ICalculator
  {
    double Add(double n, ref bool round, out bool overflow);
  }
}

Если поместить это описание в файл, то его можно будет скомпилировать с помощью компилятора С#, следующей командной строкой:

csc.exe /t:library /out:mylib.dll mylib.cs

Полученное бинарное описание можно импортировать, например, в VB, используя ключ компилятора "/r":

vbc.exe /r:mylib.dll program.vb

CLR не снимает необходимости определения типов, он позволяет разработчику делать это на любом языке, совместимом с CLR.

CLR предоставляет библиотеку, позволяющую в runtime-е читать и/или создавать сборку, содержащую описание типов. Нижеприведенный листинг демонстрирует создание сборки, содержащей описание следующего интерфейса:

namespace MyLib
{
  public interface ICalculator
  {
    double Add(double n, ref double round, out double overflow);
  }
}

Код, создающий сборку, реализован на C#, языке, похожем на C++ или Java. Мы надеемся, что у вас не возникнет проблем с пониманием кода:

using System;
using System.Reflection;
using System.Reflection.Emit;

public class emititf
{
  // Точка входа программы (объектно-ориентированный аналог функции main в С/C++)
  public static int Main(String[] argv)
  {
    // Создаем новую сборку
    AssemblyBuilder ab = DefineNewAssembly();
    // Создаем определение нового интерфейса ICalculator внутри новой сборки
    TypeBuilder tb = DefineICalculator(ab);
    // Добавляем описание метода "Add" к описанию интерфейса ICalculator
    MethodBuilder method = DefineAddMethod(tb);
    // Добавляем описание параметров
    DefineAddParameters(method);
    // Создаем тип
    Type t = tb.CreateType();
    // Записываем сборку в файл "mylib.dll"
    ab.Save("mylib.dll");
    return 0;
  }

  // Создает сборку с именем "mylib"
  static AssemblyBuilder DefineNewAssembly()
  {
    // Новая сборка создается в рамках текущего AppDomain-а
    AppDomain current = AppDomain.CurrentDomain;
    // Новая сборка нуждается в имени. Назначаем ей не строгое имя!
    AssemblyName an = new AssemblyName();
    an.Name = "mylib";
    // DefineDynamicAssembly завершает работу по созданию сборки
    return current.DefineDynamicAssembly(an, AssemblyBuilderAccess.Save);
  }

  // Создает новое описание интерфейса с именем "MyLib.ICalculator"
  static TypeBuilder DefineICalculator(AssemblyBuilder ab)
  {
    // Все описания типов находятся в модуле, определенном для нашей сборки
    ModuleBuilder mb = ab.DefineDynamicModule("mylib.dll", "mylib.dll");
    // Все описания интерфейсов должны быть помечены как Interface и Abstract
    TypeAttributes attrs = TypeAttributes.Interface|TypeAttributes.Abstract;
    // public-интерфейсы должны быть также помечены как Public
    attrs |= TypeAttributes.Public;
    // DefineType завершает работу по созданию описания для интерфейса
    return mb.DefineType("MyLib.ICalculator", attrs);
  }

  // Создает новое описание методов "double Add(double, ref double, out double)"
  static MethodBuilder DefineAddMethod(TypeBuilder itf)
  {
    // Методы интерфейса должны быть помечены как abstract, virtual и public
    MethodAttributes attrs = MethodAttributes.Public
      | MethodAttributes.Abstract | MethodAttributes.Virtual;

    // Метод определяется по имени и описанию (его параметрам)

    // Создаем описание возвращаемого значения
    Type resultType = typeof(double);
    // Создаем описание параметров
    Type[] paramTypes = new Type[]
    {
      typeof(double),
      Type.GetType("System.Boolean&"),
      Type.GetType("System.Boolean&")
    };
    // DefineMethod завершает работу по созданию описания метода
    return itf.DefineMethod("Add", attrs, resultType, paramTypes);
  }

  // Задает имя параметров и их последовательность
  static void DefineAddParameters(MethodBuilder method)
  {
    // 1-й и 2-й параметры не нуждаются в специальных атрибутах
    method.DefineParameter(1, ParameterAttributes.None, "n");
    method.DefineParameter(2, ParameterAttributes.None, "round");
    // Параметру 3 нужно задать флаг Out
    ParameterBuilder pb = method.DefineParameter(3,
                          ParameterAttributes.Out, "overflow");
    // 3-му параметру также необходимо задать атрибут Interop.Out
    AddInteropOutAttribute(pb);
  }

  // Задает атрибут Interop.Out для параметра
  static void AddInteropOutAttribute(ParameterBuilder param)
  {
    // Конструкторы идентифицируют пользовательские атрибуты
    Type attrtype = typeof(System.Runtime.InteropServices.OutAttribute);
    ConstructorInfo outattrctor = attrtype.GetConstructors()[0];
    // CustomAttributeBuilder сериализует аргументы конструктора
    CustomAttributeBuilder outattr =
                  new CustomAttributeBuilder(outattrctor, new object[0]);
    // Всю работу выполняет SetCustomAttribute
    param.SetCustomAttribute(outattr);
  }
}

Определение типа, сгенерированное этой программой, неотличимо от производимого компилятором С# (Visual Basic, C++, Perl, Python, или любого другого совместимого с CLR).

Метаданные обязательны. В COM можно было определить на С++ частные интерфейсы, не описывая их в IDL или библиотеке типов. Это позволяло создать недокументированную лазейку в свой объект. В CLR это сделать не удастся.

В CLR все типы должны быть документированы через информацию о типах, включая private-типы (скрытых типов), не рассчитанные на внешнее использование. Для поддержки скрытых типов компонента метаданные CLR позволяют пометить типы (и их отдельные члены) как доступные только изнутри описываемой сборки. Например, следующий интерфейс виден только из сборки, в которой он определен:

internal interface IBob
{
  void hibob();
}

Напротив, следующий интерфейс виден любой сборке:

public interface IBob
{
  void hibob();
}

Метаданные полностью расширяемы. Информацию о типах COM можно было расширить за счет пользовательских атрибутов. На практике это можно было сделать только через IDL, или прямой модификацией TLB. Пользовательские атрибуты в COM ассоциировали пару GUID/VARIANT с библиотекой, интерфейсом, CoClass-ом, методом, параметром, структурой или полем. Увы, VB и многие другие средства разработки не предоставляли путей задания или чтения пользовательских атрибутов, так что эта возможность бесполезна для большинства COM-разработчиков.

Информация о типах CLR расширяема из любого языка. Пользовательские атрибуты в CLR - это просто сериализованные вызовы конструктора. Разные языки имеют разный синтаксис для применения атрибутов. В С# можно просто вставить вызов конструктора в скобках, перед каким либо определением:

[ Color("Red") ]
class MyClass { }

В Visual Basic.NET можно вставить вызов конструктора в < >:

Class <Color("Red")> MyClass
End Class

В любом случае метаданные будут указывать, что цвет MyClass - красный.

Чтобы определить новые пользовательские атрибуты, следует создать новый класс, унаследовав его от System.Attribute, и реализовать в этом классе public-конструктор:

using System;

[ AttributeUsage(AttributeTargets.All) ]
public class ColorAttribute : Attribute
{
  public String color;
  public ColorAttribute(string c) { color = c;}
}

Атрибут AttributeUsage говорит, к чему будет применим новый атрибут - к классу, методу, свойству и т.п. Если класс атрибута заканчивается на Attribute, то атрибут можно будет использовать с или без этого суффикса. Например, следующие два примера идентичны, хотя последний и реже используется:

[ Color("Red") ] class MyClass { }
[ ColorAttribute("Red") ] class MyClass { }

Пользовательские атрибуты доступны в runtime через reflection-механизм. Код, приведенный выше, определяет, какой цвет присвоен данному классу и присвоен ли он вообще:

using System;
String GetColor(Object o)
{
  Type t = o.GetType();  // Получаем тип объекта
  // Получаем тип необходимого атрибута
  Type at = typeof(ColorAttribute);
  // Получаем все атрибуты этого типа
  Attribute[] rga = t.GetCustomAttributes(at);
  // Выходим, если атрибуты не заданы
  if(rga.Length == 0)
    return null;
  // Иначе извлекаем первый атрибут
  ColorAttribute color = (ColorAttribute)rga[0];
  return color.color;
}

Атрибуты - это мощный и универсальный механизм расширения метаинформации.

Динамический вызов на халяву. Поскольку информация о типах стандартна и доступна всегда для любого элемента сборки, runtime-сервисы могут динамически вызывать любой метод любого объекта. Это значит, что все объекты CLR могут быть использованы в скриптовых языках программирования. Также можно создавать сервисные визуальные компоненты, непосредственно взаимодействующие с любыми компонентами, и при этом совершенно не нужно иметь их исходных текстов.

Физическое размещение непрозрачно. CLR основывается на совершенной информации о типах. Это значит, что ни один тип не остается неописанным, вплоть до типов и имен членов данных типа. Как ни странно, физическая раскладка памяти для данного типа и его экземпляров полностью скрыты. Смещения, размеры и выравнивание членов данных неизвестны. Если нужно экспортировать CLR-объект за пределы среды исполнения, CLR создает COM-Callable Wrapper (CCW), который работает переходником между классической конвенцией вызовов COM, основанной на IUnknown и __stdcall во внутреннем формате вызовов CLR-среды.

Очень жалко, что у господ новаторов из Microsoft не хватило смелости попросту расширить стандарт COM и обеспечить поддержку этих улучшений в VS.Net. Так что всем, кто использует COM в своей работе и хочет шагать в ногу со временем, придется изучить еще и CLR. Хотя для разработчиков, чей разум не замутнен тонкостями COM, интеграция CLR и COM будет выглядеть довольно прозрачно. Чтобы использовать COM-объект в своих .Net-приложениях, необходимо будет только зарегистрировать этот объект, примерно так, как это делалось в VB 6. Для низкоуровневых программистов это значит, что для динамической генерации новых типов и перехвата доступа к уже существующим экземплярам нужна новая техника. System.Reflection.Emit дает возможность генерации новых типов; System.Runtime.Remoting дает возможность перехвата вызовов к существующим типам.

Иерархия типов с одним корнем! Почти во всех традиционных языках программирования есть <иерархия классов> и иерархия <простых типов данных>, таких, например как int, char и т.п. В COM имеется корневой тип для объектных ссылок - IUnknown. Все остальные типы отражаются в универсальном типе данных - VARIANT. И хотя в принципе VARIANT и может хранить объектные ссылки, но назвать его корнем для иерархии типов язык не поворачивается. Это подтверждается тем, что в VB 6 универсальный параметр можно было описать или как Object (то есть IDispatch), или как Variant. Да и у варианта были некоторые проблемы с информацией о типах при хранении некоторых типов данных. Например, при помещении в вариант значения Enum значение превращалось в целое число, и не было никаких возможностей узнать, к какому типу данных принадлежит значение.

В системе типов CLR нет ни IUnknown, ни VARIANT, ни (по сути) простых типов данных. Вместо этого все типы происходят от System.Object. Да, это значит, что простые типы, такие, как int или double, происходят от System.Object и являются настоящими объектами. Причем это справедливо для всех без исключения языков, поддерживающих .Net. В VB.Net и C# это становится частью языка, а в C++ осуществляется через специальное расширение - (C++ - единственный язык, который не был полностью уничтожен перед реинкарнацией. С его помощью можно будет создавать приложения сразу в бинарном виде и без CLR. По сути, родился новый язык. В новостных группах его уже окрестили MC++.). Функциональность поля vt структуры VARTYPE поглощена методом System.Object.GetType. И теперь она доступна из любого языка. Так, код на C++:

void Process(VARIANT value)
{
  switch (value.vt)
  {
    case VT_I2:
ProcessAsShort(value.iVal);     break;
case VT_R8:
ProcessAsDouble(value.dblVal);  break;
    case VT_BSTR:
ProcessAsString(value.bstrVal); break;
    case VT_UNKNOWN:
    case VT_DISPATCH:
    {
      CComQIPtr<IFoo> spFoo(value.punkVal);
      if(spFoo)
        ProcessAsFoo(spFoo);
      else
      {
        CComQIPtr<IBar> spBar(value.punkVal);
        if(spBar)
          ProcessAsBar(spBar);
      }
      break;
    }
  }
}

можно заменить на следующий код на C#:

void Process(System.Object value)
{
  if(value is short)
      ProcessAsShort((short)value);
  else if(value is double)
      ProcessAsDouble((double)value);
  else if(value is System.String)
      ProcessAsString((System.String)value);
  else if(value is IFoo)
      ProcessAsFoo((IFoo)value);
  else if(value is IBar)
      ProcessAsBar((IBar)value);
}

Заметьте, что C#-код использует специальные операторы для проверки соответствия типов, и что оператор приведения типов выполняет приведение как для объектных ссылок, так и для простых типов.

Еще раз повторюсь, что код, подобный приведенному во втором листинге, можно написать на любом CLR-совместимом языке.

Классы с публичными членами. В COM классы были именованными реализациями одного или более интерфейсов. И точка. Классам не позволялось иметь методы или свойства, не принадлежащие какому- либо интерфейсу. В CLR у классов могут быть public-члены. Это позволяет использовать методы как для реализации интерфейсов компонентов, так и в качестве одиноких методов в классах конечных приложений.

Множественное наследование. В CLR есть две новации. Одна - реализация множественного наследования для интерфейсов. По всей видимости, это расширение сделано с целью избавиться от QueryInterface. QueryInterface должен быть заменен на операцию приведения типов. С точки зрения идеологии красивое решение, но не приведет ли оно к проблемам интеграции с COM? Вторая новация связана с запретом множественного наследования. Множественное наследование наряду с шаблонами и переопределением операторов были <визитной карточкой> C++. Похоже, времена меняются. Множественное наследование не запрещено в чистом C++. Но его использование ограничено в MC++. VB.Net и C# вообще не поддерживают множественного наследования, но об этом мы еще поговорим при рассмотрении этих языков.

Делегаты. Делегирование - это по сути домысленная и доведенная до совершенства технология обратных (callback) вызовов. В COM все вызовы методов делались по объектным ссылкам, принадлежащим некому типу интерфейса. Чтобы создать обратную связь с объектом, обычно описывался новый событийный интерфейс, и создавалась его реализация. При этом если нужно было создать связь всего лишь по одному методу, все равно приходилось реализовывать весь интерфейс.

CLR-делегаты используются для привязки вызова метода объекта к переменной. Это гибрид указателей на функции в С или, точнее, на функции-члены в С++. Функционально делегаты похожи на интерфейсы с единственным методом, с тем отличием, что целевому типу нужно только иметь метод, чья сигнатура соответствует сигнатуре делегата.

Посмотрите на следующий пример:

public delegate void Hook();
public class MyClassEx
{
  Hook hook;
  public void RegisterHook(Hook h) { hook = h; }
  public void f()
  {
    if (hook != null)
      hook(); // callback-вызов
  }
}

Заметьте, что вместо интерфейса определяется простой тип-делегат. Заметьте ещё, что вызов перехватчика использует синтаксис, похожий на указатели функций С/C++. Разные языки VS.Net имеют различный синтаксис работы с делегатами, но внутренний механизм един.

Чтобы написать перехватчик MyClassEx, нужно просто реализовать метод, чья сигнатура соответствует сигнатуре делегата Hook:

public class MyHookEx
{
  public void AnyNameIWant()
  {
    System.Console.WriteLine("Hook вызван!");
  }
}

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

MyClassEx mc = new MyClassEx();
mc.RegisterHook(new Hook(MyHookEx.AnyNameIWant));
mc.f(); // вызывает MyHookEx.AnyNameIWant()

Обратите внимание на несколько необычный синтаксис инициализации делегата. Компилятор C# скрыто генерирует код инициализации нового объекта-делегата с маркером метаданных для означенного метода.

На таком же принципе построена система обработки событий. Но регистрация обработчика подразумевает возможность подключения нескольких обработчиков событий для одного источника. В C# такое подключение делается с помощью оператора <+=>.

Полная поддержка аспектно-ориентированного программирования. MTS и COM+ представили массам аспектно- ориентированное программирование, позволив разработчикам переместить независимые от логики приложения аспекты из исходного кода в декларативные атрибуты. MTS и COM+ ввели также понятие как контекст для управления областью исполнения объекта. Контексты подразделяют процессы и содержат упорядоченную коллекцию именованных контекстных свойств, контролируемых атрибутами класса наподобие Synchronization, ThreadingModel, Transaction и т.д. CLR продвигает эту концепцию дальше и значительно расчищает реализацию.

В MTS и COM+ наборы атрибутов класса и свойств контекста были фиксированными. В CLR можно определить новый атрибут класса, вносящий новые свойства в контекст объекта. Это позволяет стороннему разработчику определять сервисы, поведение которых будет задаваться через атрибуты, контекстные свойства и перехват.

В COM+ объектные ссылки были ограничены контекстом, и для использования объектных ссылок в глобальных переменных нужна была Global Interface Table (GIT). В CLR областью действия объектных ссылок является AppDomain (эквивалент процесса в CLR), и объектные ссылки можно использовать в глобальных переменных без всякого маршалинга.

В COM+ все объекты прикреплены к контексту, в котором были инициализированы, и по умолчанию маршалятся в другие контексты по ссылке. В результате даже те объекты, которым не нужны сервисы типа транзакций или декларативной безопасности, оказываются замкнуты в конкретном контексте процесса. Чтобы избежать этого, разделяемые объекты часто агрегируют freethreaded-маршалер (FTM), делающий их контекстно-независимыми при вызове изнутри процесса, но маршалящий по значению за границы процесса. Объекты, требующие межпроцессного доступа через копию объекта, вместо proxy обычно реализуют IMarshal для обеспечения семантики маршалинга по значению.

В CLR по умолчанию используется маршалинг по значению между AppDomains и контекстная независимость внутри AppDomains, см. рисунок 1.

Рис. 1. Объект CLR

Это значит, что по умолчанию объект никогда не получит proxy. Вместо этого внутри исходного AppDomain-а доступ к объекту будет осуществляться напрямую, а доступ между AppDomain-ами производится путем копирования объекта. Как показано на рисунке 2, классы, унаследованные от System.MarshalByRefObject, контекстно-независимы внутри AppDomains, но маршалятся по ссылке между AppDomain-ами (грубый говоря, это эквивалент агрегирования FTM в COM+).

Рис. 2. MarshalByRefObject в CLR

Объекты, наследующие функциональность от System.ContextBoundObject, приколоты к контексту, в котором они инициализированы (см. рисунок 3), точно так же, как по умолчанию в COM+.

Рис. 3. ContextBoundObject в CLR

И все это доступно без явного кодирования, просто изменением базового класса.

Подмена концепций или <Король пока жив, но да здравствует новый король!>

Итак, наш небольшой анализ показывает, что ориентация Microsoft на COM заменяется ориентацией на CLR. Но остается вопрос, так что же, COM умер? По сути, COM жив и жалеет всех живых. Во-первых, у CLR пока нет собственных средств межмашинного взаимодействия. Такое взаимодействие осуществляется с помощью старого доброго COM. Во-вторых, большое количество современных продуктов целиком и полностью ориентировано на COM и ActiveX.

Но совместимость между COM и CLR далеко не стопроцентная. Это обусловлено различиями в архитектуре и неполной поддержкой со стороны самих средств разработки. Но CLR дает существенный выигрыш программистам, сейчас ориентированным на COM. Практически все аспекты модели программирования COM уцелели (интерфейсы, классы, атрибуты, контексты и т.д.). Однако CLR - это другая, лучшая модель компонентного программирования. Она упрощает межъязыковую интеграцию, позволяя расширять (путем наследования) функциональность классов созданных на других языках.

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

Как отобразить индикатор прогресса на строке состояния?
Автор: Александр Шаргин

Чтобы решить эту задачу, достаточно вспомнить, что строка состояния - это самое обыкновенное окно, на котором можно создавать дочерние окна. В данном случае нам потребуется создать контрол типа progress bar, задав для него стиль WS_CHILD и строку состояния в качестве родительского окна. Когда индикатор прогресса создан, мы работаем с ним, а затем уничтожаем его.

Следующий фрагмент демонстрирует создание индикатора прогресса на строке состояния.

// Получаем указатель на главное окно.
CMainFrame *pFrame = dynamic_cast<CMainFrame *>(AfxGetMainWnd());

// Находим объект строки состояния.
CStatusBar &sb = pFrame->m_wndStatusBar;

// Определяем прямоугольник, в котором будет размещаться индикатор прогресса.
// В нашем примере он будет занимать всю первую панель строки состояния.
CRect rect;
sb.GetItemRect(0, rect);

// Создаём индикатор прогресса.
CProgressCtrl pc;
pc.Create(WS_CHILD | WS_VISIBLE, rect, &sb, 0);
pc.SetRange(0, 100);
pc.SetPos(0);
pc.SetStep(1);

// Имитируем выполнение длительного процесса.
for(int i=0; i<100; i++)
{
    Sleep(30);
    pc.StepIt();
}

// Уничтожаем индикатор прогресса.
pc.DestroyWindow();
ПРИМЕЧАНИЕ
В этом фрагменте используется приведение типов с помощью dynamic_cast. Этот оператор в свою очередь использует механизм RTTI (информацию о типах на этапе выполнения). Поэтому необходимо включить поддержку RTTI, чтобы приведённый фрагмент мог работать в вашей программе. Поддержка RTTI включается, если задать компилятору ключ /GR. В настройках проекта (Project->Settings) ему соответствует настройка Enable Run-Time Type Information (RTTI) (вкладка C/C++, категория C++ Language).

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

Алекс Jenter jenter@rsdn.ru
Duisburg, 2001. Публикуемые в рассылке материалы принадлежат сайту RSDN.


ПРОГРАММИРОВАНИЕ НА V I S U A L C + +
РАССЫЛКА САЙТА
RSDN.RU
Выпуск No. 59 от 13 января 2001 г.

РАССЫЛКА ЯВЛЯЕТСЯ ЧАСТЬЮ ПРОЕКТА RSDN , НА САЙТЕ КОТОРОГО ВСЕГДА МОЖНО НАЙТИ ВСЮ НЕОБХОДИМУЮ РАЗРАБОТЧИКУ ИНФОРМАЦИЮ, СТАТЬИ, ФОРУМЫ, РЕСУРСЫ, ПОЛНЫЙ АРХИВ ПРЕДЫДУЩИХ ВЫПУСКОВ РАССЫЛКИ И МНОГОЕ ДРУГОЕ.

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

Ну, надеюсь все хорошо отдохнули за праздники, и готовы с новыми силами читать рассылку! ;-)

CТАТЬЯ

Регулярные выражения
Автор: Михаил Купаев
Источник: <Технология Клиент-Сервер>

Пример RegExpTest.zip - 2 KB
Пример RegexNetTest.zip - 11 KB

Словосочетание <регулярные выражения>, прямой перевод английского , звучит довольно неуклюже. Однако оно уже настолько прижилось, что попало в словари, поэтому придется использовать именно его - за неимением лучшего.

Регулярные выражения - это один из способов поиска подстрок (соответствий) в строках. Осуществляется это с помощью просмотра строки в поисках некоторого шаблона. Общеизвестным примером могут быть символы <*> и , используемые в командной строке DOS. Первый из них заменяет ноль или более произвольных символов, второй же - один произвольный символ. Так, использование шаблона поиска типа "text?.*" найдет файлы textf.txt, text1.asp и другие аналогичные, но не найдет text.txt или text.htm. Если в DOS использование регулярных выражений было крайне ограничено, то в других местах (то есть операционных системах и языках программирования) они почти достигли уровня высокого искусства. <Почти> потому, что предметы высокого искусства практически невозможно употреблять в повседневной жизни. Более сложным примером применения регулярных выражений может быть удаление мусора, внесенного Microsoft Word при сохранении документа в формате HTML. Разработчики Word умудрились все сделать по-своему, в результате чего HTML-документ порой становится больше исходного DOC-файла за счет огромного количества понятных только IE5 тегов, вычистить которые вручную нет никакой возможности.

Особенно полезны регулярные выражения в программах, написанных на скриптовых (интерпретируемых) языках, например, VBScript, JScript и Perl. Из-за того, что весь их код интерпретируется, разбор текстовых строк и выражений выполняется неприемлемо медленно. Применение регулярных выражений дает значительное увеличение производительности, поскольку библиотеки, интерпретирующие регулярные выражения, обычно пишутся на низкоуровневых высокопроизводительных языках (С, С++, Assembler). Например, в MSDN с помощью регулярных выражений осуществляется динамическое форматирование HTML-страниц:

Рис.1. Всплывающее окно See Also создается динамически
c помощью регулярных выражений.

Обычно с помощью регулярных выражений выполняются три действия:

  • Проверка наличия соответствующей шаблону подстроки.
  • Поиск и выдача пользователю соответствующих шаблону подстрок.
  • Замена соответствующих шаблону подстрок.

Наибольшее развитие регулярные выражения получили в Perl, где их поддержка встроена непосредственно в интерпретатор. В других языках, как правило, используются реализующие регулярные выражения дополнения и модули сторонних разработчиков. В VBScript и JScript используется объект RegExp, в С/С++ можно использовать библиотеки Regex++ и PCRE (Perl Compatible Regular Expression), а также ряд менее известных библиотек, для Java существует целый набор расширений - ORO , RegExp, Rex и gnu.regexp.

Особняком стоит Microsoft Visual Studio.Net, существующая пока только в beta-версии, но уже удостоившаяся массы публикаций и разговоров. Реализация регулярных выражений в .Net (Regex) полностью совместима с Perl, и даже несколько расширена. Все, что говорится про Perl, вполне применимо к .Net.

В составе ATL 7, также входящего в VC.Net, имеется шаблон XXX, который позволяет встраивать регулярные выражения в С++-программы (независимо от CLR). Он доступен в исходных текстах, поэтому его можно довольно просто приспособить к своим надобностям, то есть встроить в него поддержку нужного языка или добавить необходимую функциональность. Этот шаблон по всей видимости, должен оказаться самой быстрой реализацией регулярных выражений, поскольку весь код подставляется компилятором как inline и, соответственно, компилятор может качественно оптимизировать код. Прямая работа с любыми видами строк (вид строки задается в качестве параметра шаблона) также повышает производительность.

Реализации регулярных выражений различаются, однако в целом они очень похожи друг на друга, и, как правило, программист, однажды освоивший использование регулярных выражений, в дальнейшем практически не встречает затруднений.

Синтаксис регулярных выражений до сих пор не полностью стандартизован. Существует POSIX-версия регулярных выражений, полностью описывающая стандарт синтаксиса для POSIX. Но версия Perl шире и более гибка, чем и объясняется ее широкая распространенность. Большинство библиотек по синтаксису и используемым метасимволам совместимо с Perl, поэтому имеет смысл начать разбираться с использованием регулярных выражений на примере именно этого языка.

Три типа машин регулярных выражений

На практике применяются три типа машин регулярных выражений.

  1. DFA (Deterministic Finite-state Automaton - детерминированные конечные автоматы) машины работают линейно по времени, поскольку не нуждаются в откатах (и никогда не проверяют один символ дважды). Они могут гарантированно найти самую длинную строку из возможных. Однако, поскольку DFA содержит только конечное состояние, он не может найти образец с обратной ссылкой и, из-за отсутствия конструкций с явным расширением, не ловит подвыражений. Они используются, например, в awk, egrep или lex.
  2. Традиционные NFA-машины (NonDeterministic Finite-state Automaton - недетерминированные конечные автоматы) используют "жадный" алгоритм отката, проверяя все возможные расширения регулярного выражения в определенном порядке и выбирая первое подходящее значение. Поскольку традиционный NFA конструирует определенные расширения регулярного выражения для поиска соответствий, он может искать подвыражения и backreferences. Но из-за откатов традиционный NFA может проверять одно и то же место несколько раз. В результате работает он медленнее. Поскольку традиционный NFA принимает первое найденное соответствие, он может и не найти самое длинное из вхождений. Именно такие механизмы регулярных выражений используются в Perl, Python, Emacs, Tcl и .Net.
  3. POSIX NFA - машины похожи на традиционные NFA-машины, за исключением "терпеливости" - они продолжают поиск, пока не найдут самое длинное соответствие. Поэтому POSIX NFA-машины медленнее традиционных, и поэтому же нельзя заставить POSIX NFA предпочесть более короткое соответствие длинному. Одно из главных достоинств POSIX NFA-машины - наличие стандартной реализации.

Чаще всего программисты используют традиционные NFA-машины, поскольку они точнее, чем DFA или POSIX NFA. Хотя в наихудшем случае время их работы растет по экспоненте, использование образцов, снижающих уровень неоднозначности и ограничивающих глубину поиска с возвратом (backtracking), позволяет управлять их поведением, уменьшая время поиска до приемлемых значений.

Различия синтаксиса регулярных выражений

Реально только в синтаксис Perl использование регулярных выражений встроено непосредственно. В остальных языках для этого используются методы классов. Так, например, в C# работа с регулярными выражениями выглядит следующим образом:

Regex re = new Regex("pattern", "options");
MatchCollection mc = re.Matches("this is just one test");
iCountMatchs = mc.Count;

где re - это новый объект-Regex, в чьем конструкторе передается образец поиска (pattern) и опции (options) (Таблица 1), задающие различные варианты поиска

Символ | Значение
I | Поиск без учета регистра.
m | Многострочный режим, позволяющий находить совпадения в начале или конце строки, а не всего текста.
n | Находит только явно именованные или нумерованные группы в форме (?<name>:). Значение этого будет объяснено ниже, при обсуждении роли скобок в регулярных выражениях.
c | Компилирует. Генерирует промежуточный MSIL-код, перед исполнением превращающийся в машинный код.
s | Позволяет интерпретировать конец строки как обыкновенный символ-разделитель. Часто это значительно упрощает жизнь.
x | Исключает из образца неприкрытые незначащие символы (пробелы, табуляция и т.д.) и включает комментарии в стиле Perl (#). Есть некоторая вероятность, что к выходу в свет эти комментарии могут исчезнуть.
r | Ищет справа налево.

Сочетание флагов m и s дает очень удобный режим работы, учитывающий концы строк и позволяющий пропустить все незначащие символы, включая символ конца строки.

Ниже приведен пример на VB 6, использующий внешнюю библиотеку VBScript RegExp, поставляемую с MS Scripting Host. Ее можно скачать с сайта Microsoft (или найти vbscript.dll в большинстве его продуктов). Этот пример разбирает строку и помещает найденные вхождения в список List1.

Dim re As New VBScript_RegExp.RegExp
Dim matchs As MatchCollection
re.Pattern = "pattern"
re.Global = True ' для поиска по всему тексту.
Set matchs = re.Execute("this is just one test")
Dim m As VBScript_RegExp.Match
List1.Clear
For Each m In matchs
    List1.AddItem m.Value & " Ndx " & m.FirstIndex & " Len " & m.Length
Next

В других языках все выглядит аналогично.

Perl разделяет составные части определения регулярного выражения символами "/". Выглядит это примерно так:

expression =~ m/pattern/[switches]

Такое выражение выполняет поиск подстроки, соответствующий шаблону 'pattern' в строке expression и возвращает найденные подстроки ($1, $2, $3, ...). "m" означает "match", т.е. соответствие. Например,

$test = "this is just one test";
$test =~ m/(o.e)/

вернет "one" в $1.

Для замены применяется выражение

expression =~ s/pattern/new text/[switches]

Это выражение, как несложно догадаться, заменяет "pattern" на "new text". Например:

$test = "this is just one test";
$test =~ s/one/my/

заменит one на my, в результате давая "this is just my test", сохраняемое в $test.

В Perl используются те же опции, что и в .Net, кроме "n" и "r". В других реализациях библиотек регулярных выражений опций меньше, либо вовсе нет. Так, в приведенном выше примере на VB настройки производятся через свойства объекта RegExp. Ниже примеры будут даваться в основном в стиле Perl.

Основы синтаксиса регулярных выражений

Я не стану пытаться написать полный справочник по всем символам, используемым в шаблонах регулярных выражений. Для этого есть другие источники. Здесь мы приведем только основные метасимволы.

В двойных кавычках далее будут употребляться значения, выдаваемые регулярными выражениями, а в одинарных - синтаксис регулярных выражений.

В Perl метасимволы, которые вы хотите использовать не как таковые, а как собственно символы, должны быть прикрыты escape-символом \, как в С++ (в других языках может быть иначе, например, в VB это не нужно). То есть, чтобы найти "[", нужно писать '\['. Символ \ означает, что идущий за ним символ - это спецсимвол, константа и так далее. Например, 'n' означает букву "n". '\n' означает символ новой строки. Последовательность '\\' соответствует "\", а '\(' соответствует "(".

Символ '.' соответствует любому символу, кроме '\n' (если не используется опция 's', увы, доступная только в Perl 5-совместимых реализациях). Чтобы найти любой символ, включая \n, используйте что-нибудь вроде '[.\n]'.

Искомые выражения

Выражением может быть один символ или последовательность символов, заключенных в круглые или квадратные скобки. Особенности использования скобок будут описаны ниже.

Классы символов (Character class)

Используя квадратные скобки, можно указать группу символов (это называют классом символов) для поиска. Например, конструкция 'б[аи]ржа' соответствует словам <баржа> и <биржа>, т.е. словам, начинающимся с <б>, за которым следуют <а> или <и>, и заканчивающимся на <ржа>.

Возможно и обратное, то есть, можно указать символы, которых не должно содержаться в найденной подстроке. Так, '[^1-6]' находит все символы, кроме цифр от 1 до 6. Следует упомянуть, что внутри класса символов '\b' обозначает символ backspace (стирания).

Квантификаторы, они же умножители (Quantifiers)

Если неизвестно, сколько именно знаков должна содержать искомая подстрока, можно использовать спецсимволы, именуемые мудреным словом квантификаторы (quantifiers). Например, можно написать "hel+o", что будет означать слово, начинающееся с "He", со следующими за ним одно или несколько "l", и заканчивающееся на "о". Следует понять, что квантификатор относится к предшествующему выражению, а не отдельному символу.

Список квантификаторов вы можете найти в таблице 2.

Символ | Описание
* | Соответствует 0 или более вхождений предшествующего выражения. Например, 'zo*' соответствует "z" и "zoo".
+ | Соответствует 1 или более предшествующих выражений. Например, "zo+" соответтсвует "zo" and "zoo", но не "z".
? | Соответствует 0 или 1 предшествующих выражений. Например, 'do(es)?' соответствует "do" в "do" or "does".
{n} | n - неотрицательное целое. Соответствует точному количеству вхождений. Например, 'o{2}' не найдет "o" в "Bob",но найдет два "o"' в "food".
{n,} | n - неотрицательное целое. Соответствует вхождению, повторенному не менее n раз. Например, 'o{2,}' не находит "o" в "Bob", зато находит все "o" в "foooood". 'o{1,}' эквивалентно 'o+'. 'o{0,}' эквивалентно 'o*'.
{n,m} | m и n - неотрицательные целые числа, где n <= m. Соответствует минимум n и максимум m вхождений. Например, 'o{1,3} находит три первые "o" в "fooooood". 'o{0,1}' эквивалентно 'o?'. Пробел между запятой и цифрами недопустим.
Жадность

Важной особенностью квантификаторов '*' и '+' является их всеядность. Они находят все, что смогут - вместо того, что нужно. То есть,

$test = "hello out there, how are you";
$test =~ m/h.*o/

означает "искать 'h', за которым следует несколько произвольных символов, за которыми следует 'o'". В виду, наверное, имелось "hello", но найдено будет "hello out there, how are yo" - из-за жадности регулярного выражения, ищущего не первую, а последнюю "о". Излечить квантификатор от жадности можно, добавив '?'. То есть,

$test = "hello out there, how are you";
$test =~ m/h.*?o/

найдет именно "hello", что и было нужно, поскольку ищет 'h', за которым следует несколько произвольных символов, до первого встреченного 'o'".

Концы и начала строк

Проверка начала или конца строки производится с помощью метасимволов ^ и $. Например, "^thing" соответствует строке, начинающейся с "thing". "thing$" соответствует строке, заканчивающейся на "thing". Эти символы работают только при включенной опции 's'. При выключенной опции 's' находятся только конец и начало текста. Но и в этом случае можно найти конец и начало строки, используя escape-последовательности \A и \Z. Все это относится только к Perl-совместимым реализациям. Остальные же будут искать только конец и начало текста. В .Net имеется еще и символ \z, точный конец строки.

Граница слова

Для задания границ слова используются метасимволы '\b' и '\B'.

$test =~ m/out/

соответствует не только "out" в "speak out loud", но и "out" в "please don't shout at me". Чтобы избежать этого, можно предварить образец маркером границы слова:

$test =~ m/\bout/

Теперь будет найдено только "out" в начале слова. Не стоит забывать, что ВНУТРИ класса символов '\b' обозначает символ backspace (стирания).

Приведенные в Таблице 3 метасимволы не заставляют машину регулярных выражений продвигаться по строке или захватывать символы. Они просто соответствуют определенному месту строки. Например, ^ определяет, что текущая позиция - начало строки. '^FTP' возвращает только те "FTP", что находятся в начале строки.

Символ | Значение
^ | Начало строки.
$ | Конец строки, или перед \n в конце строки(см. опцию m).
\A | Начало строки (ignores the m option).
\Z | Конец строки, или перед \n в конце строки (игнорирует опцию m).
\z | Точно конец строки (игнорирует опцию m).
\G | Начало текущего поиска (Часто это в одном символе за концом последнего поиска).
\b | На границе между \w (алфавитно-цифровыми) и \W (не алфавитно-цифровыми) символами. Возвращает true на первых и последних символах слов, разделенных пробелами.
\B | Не на \b-границе.
Вариации и группировка

Символ '|' можно использовать для перебора нескольких вариантов. Использование этого символа совместно со скобками - '(...|...|...)' - позволяет создать группы вариантов. Скобки используются для "захвата" подстрок для дальнейшего использования и сохранения их во встроенных переменных $1, $2, ..., $9.

Например,

$test = "I like apples a lot";
$test =~ m/like (apples|pines|bananas)/

сработает, поскольку "apples" - один из трех перечисленных вариантов. Скобки также поместят "apples" в $1 как обратную ссылку для дальнейшего использования. В основном это имеет смысл при замене, см. "Различия синтаксиса регулярных выражений".

Обратные ссылки, Lookahead- и Lookbehind-условия
Обратные ссылки

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

Например,

$test = "Today is monday the 18th.";
$test =~ m/([0-9]+)th/

сохранит "18" в $1, а

$test = "Today is monday the 18th.";
$test =~ m/[0-9]+th/

ничего не станет сохранять - из-за отсутствия скобок.

$test = "Today is monday the 18th.";
$test =~ m/(?:[0-9]+)th/

также ничего не станет сохранять благодаря использованию оператора '?:'.

Следующий пример демонстрирует, как можно использовать эту возможность в операции замены:

$test = "Today is monday the 18th.";
$test =~ s/ the ([0-9]+)th/, and the day is $1/

приведет к записи "Today is monday, and the day is 18." в переменную $test.

Можно ссылаться на подстроки, уже найденные данным запросом, используя \1, \2, ..., \9. Следующее регулярное выражение удалит повторяющиеся слова:

$test = "the house is is big";
$test =~ s/\b(\S+)\b(\s+\1\b)+/$1/

записывает "the house is big" в $test.

Lookahead- и Lookbehind-условия

Иногда нужно сказать "найдите вот это, но только если перед ним не стоит вот этого", или "найдите вот это, но только если за ним не стоит вот этого". Пока речь идет об одиночном символе, достаточно воспользоваться [^...].

В более сложном случае придется использовать так называемые lookahead-условия или lookbehind-условия. Не путайте Positive lookahead с оптимистичным взглядом в будущее. Всего есть четыре типа таких условий:

  • Положительное lookahead-условие '(?=re)'
    Соответствует, только если за ним следует регулярное выражение re.
  • Отрицательное lookahead-условие '(?!re)'
    Соответствует, только если за ним не следует регулярное выражение re.
  • Положительное lookbehind-условие '(?<=re)'
    Соответствует, только если перед ним следует регулярное выражение re.
  • Отрицательное lookbehind-условие '(?<!re)'
    Соответствует, только если перед ним не следует регулярное выражение re.

Примеры:

$test = "HTML is a document description-language and not a programming-language";
$test =~ m/(?<=description-)language/

Найдет первое "language" ("description-language"), как предваряемое "description-", а

$test = "HTML is a document description-language and not a programming-language";
$test =~ m/(?<!description-)language/

Найдет второе "language" ("programming-language").

Следующие примеры выполнены в .Net. Поиск осуществляется в следующем тексте:

void aaa
{
  if(...)
  {
    try { ... }
    catch(Exception e1)  {  MessageBox.Show(e1.ToString(), "Error");  }
    finally  {  listBox1.EndUpdate();  }
  }
}
Положительный Lookahead

Шаблон \{(?=[^\{]*\}).*?\} находит самый глубоко вложенный блок, выделенный фигурными скобками. Результат выполнения:

  1. { ... }
  2. { MessageBox.Show(e1.ToString(), "Error"); }
  3. { listBox1.EndUpdate(); }
Положительный Lookbehind

Шаблон (?<=try\s*)\{(?=[^\{]*\}).*?\} находит самый глубоко вложенный блок выделенный фигурными скобками, перед которым есть try. Результат выполнения: { ... }.

Отрицательный Lookbehind

Шаблон (?<!try\s*)\{(?=[^\{]*\}).*?\} находит самый глубоко вложенный блок выделенный фигурными скобками перед которым нет слова try. Результат выполнения:

  1. { MessageBox.Show(e1.ToString(), "Error"); }
  2. { listBox1.EndUpdate(); }

В этих примерах жирным выделены Lookahead- и Lookbehind-условия.

Еще примеры

Вот еще несколько примеров использования регулярных выражений, более приближенных к реальной жизни.

Перестановка двух первых слов:
s/(\S+)(\s+)(\S+)/$3$2$1/

В других языках замена обычно делается отдельным методом, одним из параметров передается шаблон замены, где можно использовать переменные $1, $2, $3 и т.д.

Поиск пар name=value:
m/(\w+)\s*=\s*(.*?)\s*$/

Здесь имя - в $1, а значение - в $2.

Чтение даты в формате YYYY-MM-DD:
m/(\d{4})-(\d\d)-(\d\d)/

Теперь YYYY - в $1, MM - в $2, DD - в $3.

Выделение пути из имени файла:
m/^.*(\\|\/)

В "Y:\KS\regExp\!.Net\Compilation\ms-6D(1).tmp" такое выражение найдет "Y:\KS\regExp\!.Net\Compilation\"

Будучи примененным к файлу С++, выделяет комментарии, строки и идентификаторы "new", "static char" и "const". Работает и на старом RegExp:

("(\\"|\\\\|[^"])*"|/\*.*\*/|//[^\r]*|#\S+|\b(new|static char|const)\b)

Выделяет тег <a href=":"> в HTML-коде:

<\s*a("[^"]*"|[^>])*>
Регулярные выражения в .Net

Как уже упоминалось выше, регулярные выражения широко используются практически во всех языках программирования. Каждый из языков накладывает свой отпечаток на синтаксис регулярных выражений, хотя суть и не меняется. Так, например, то, что в JScript пишется /a.c/, в VBScript, естественно, будет "a.c".

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

Символ | Значение
\w | Слово. То же, что и [a-zA-Z_0-9].
\W | Все, кроме слов. То же, что и [^a-zA-Z_0-9].
\s | Любое пустое место. То же, что и [ \f\n\r\t\v].
\S | Любое непустое место. То же, что и [^ \f\n\r\t\v].
\d | Десятичная цифра. То же, что и [0-9].
\D | Не цифра. То же, что и [^0-9].

Кстати, регулярные выражения в .Net умеют понимать русский язык. Особенно интересно и слегка непривычно то, что они делают это корректно. В Help'е сказано, например, что при поиске границы слова с использованием \b работают символы [a-zA-Z_0-9], однако верить этому не следует. На практике это не так. Русские буквы ищутся и находятся не хуже латиницы. Впрочем, может быть, к release-версии все будет приведено к соответствию с Help'ом.

Классы, определяющие регулярные выражения .NET - это часть библиотеки базовых классов Microsoft .NET Framework, что означает одинаковую реализацию регулярных выражений для всех языков и средств, работающих с CLR (Common Language Runtime) - естественно, за вычетом языковых особенностей, типа уже упоминавшихся escape-символов.

В .Net появились условные сравнения (conditional evaluation). Позволяет варьировать используемые шаблоны в зависимости от результатов поиска предыдущего подвыражения. Это заставит, например, пропустить правую скобку, если левая уже была найдена подвыражением. К сожалению, информация об этом пока слишком обрывочна, чтобы говорить об этом подробнее.

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

Кроме перечисленных, есть еще и масса других, менее значительных дополнений и расширений, но перечислять их все нет ни сил, ни желания. Особенно учитывая, что всё может измениться без предупреждения.

Большая ложка дегтя

Увы, Microsoft традиционно пребывает в состоянии творческого безумия, и правая рука у него не знает, что делает левая (подробнее об этом см. "Средства программирования). Поэтому в саму среду Microsoft .Net встроена ДРУГАЯ библиотека регулярных выражений. Если они это изменят до выхода финальной версии (все, что вы здесь читаете, написано на базе beta 1), честь им и хвала. Если же не изменят (например, по забывчивости), разработчикам, скорее всего, придется работать по принципу "одним пользуемся, другое продаем".

Компиляция и повторное использование регулярных выражений

По умолчанию Regex компилирует регулярные выражения в последовательность внутренних байт-кодов регулярных выражений (это высокоуровневый код, отличный от Microsoft intermediate language (MSIL)). При исполнении регулярных выражений байт-код интерпретируется.

Если же конструировать объект Regex с опцией 'с', он компилирует регулярные выражения в MSIL-код вместо упомянутого байт-кода. Это позволяет JIT-компилятору Microsoft .NET Framework преобразовать выражение в родные машинные коды для повышения производительности.

Но сгенерированный MSIL нельзя выгрузить. Единственный способ выгрузить код - это выгрузить из памяти приложение целиком. Это значит, что занимаемые скомпилированным регулярным выражением ресурсы нельзя освободить, даже если сам объект Regex уже освобожден и уничтожен сборщиком мусора.

Из-за этого казуса приходится задумываться - стоит ли компилировать регулярные выражения с опцией 'с', и если да, то какие и сколько. Если приложение должно постоянно использовать множество регулярных выражений, придется обойтись интерпретацией. А вот если есть несколько постоянно используемых регулярных выражений, можно и скомпилировать их для ускорения работы.

Для повышения производительности Regex кэширует в памяти все регулярные выражения. Поэтому повторного разбора при каждом очередном использовании не происходит. Такой подход несколько уменьшает разницу в производительности компилируемых и интерпретируемых регулярных выражений.

Приложение RegExpTest

В качестве примера использования регулярных выражений мы создали .Net-приложение, использующее регулярные выражения для поиска в тексте.

Рис.2. Приложение RegExpTest

Мода - великая вещь, поэтому писать приложение следует не на Java, не на VB, а на C#. Это модно, и доказывает, что автор не стоит на месте, а работает над собой.

Отрывки кода этого примера приведены в Листинге 1. Само приложение можно скачать с нашего ftp-сайта.

Листинг 1. Использование регулярных выражений в C#

// Класс для хранения информации о найденном вхождении
protected class MyItem
{
   public MyItem(string Match, int Index, int Len)
   {
      this.Match = Match;
      this.Index = Index;
      this.Len = Len;
   }
   public override string ToString()
   {
      return Index.ToString() + ", " + Len.ToString() + ", " + Match;
   }
   public string Match;
   public int Index;
   public int Len;
}
...
protected void Parce()
{
   int iCountMatchs = 0;
   try
   {
     // Очищаем лист-бокс
     listBox1.Items.Clear();
     statusBar1.Text = "Parsing...";

     // создаем объект re, задавая в его конструкторе
     // шаблон и опции
     Regex re = new Regex(tbPattern.Text, tbOptions.Text);

     // Выполняем поиск заданного выше шаблона внутри
     // текста и текстового поля tbTextForSearch
     MatchCollection mc = re.Matches(tbTextForSearch.Text);
     iCountMatchs = mc.Count;

     // Выводим информацию о количестве найденных вхождений...
     statusBar1.Text = "Load list (" + iCountMatchs.ToString() + ")...";

     // ...и заносим информацию о них в лист-бокс.
     listBox1.BeginUpdate();
     foreach(Match m in mc)
     {
       // Для хранения информации о найденном вхождении
       // мы используем созданный нами класс MyItem.
       // Элементы управления (типа лист-бокса) в .Net
       // позволяют хранить вместо текстового значения
       // объект, а при отображении текста в строке вызывают
       // метод - ToString. Так что объект любого класса,
       // реализующего метод ToString, может выступать в
       // качестве элемента лист-бокса.
       listBox1.Items.Add(new MyItem(m.ToString(), m.Index, m.Length));
     }
   }
   catch(Exception e1)
   {
     MessageBox.Show(e1.ToString(), "Error");
   }
   finally
   {
     listBox1.EndUpdate();
     statusBar1.Text = "Done " + iCountMatchs.ToString();
   }
}
Заключение

Это только краткое ведение в регулярные выражения и их использование. Если вы хотите лучше разобраться в этом, попробуйте потренироваться в создании регулярных выражений самостоятельно. Практика показывает, что разбор чужих регулярных выражений практически бесполезен, читать их почти невозможно. Однако лучше научиться пользоваться ими - это часто упрощает жизнь.

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

Как вывести на экран картинку в JPEG/GIF/PNG/др. формате?
7 способов как это сделать
Автор: Павел Блудов

Демонстрационное приложение (WTL) DrawImg (50kb)

Рис.: Приложение DrawImg

Сегодня практически все программы используют различные картинки в качестве элементов интерфейса. Даже существует API функция ::LoadImage(), умеющая загружать файлы в формате bmp, ico и cur. Этого достаточно для панелей управления и диалогов. Но если размер картинки превышает 100x100 пикселов и их нужно несколько, файлы формата bmp использовать не удобно. Хочется что-то вроде jpg или gif.

Тут ::LoadImage() нам уже не помошник. Придется использовать специальные библиотеки. Наибольшей популярностью пользуются:

  • Independent JPEG Group
  • Portable Network Graphics
  • TIFF Software
  • Intel(R) JPEG Library
  • Image Library
  • CXImage
  • Small JPEG Decoder Library

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

Способ 1 (OleLoadPicture)

Самый "официальный" способ. Появился вместе с OLE32 и работает до сих пор. Функции OleLoadPicture(Ex) и OleLoadPicturePath умеют загружать картинки в формате BMP, GIF, JPEG, ICO, WMF, и EMF:

#include <olectl.h>

    HRESULT Load(LPCTSTR szFile)
    {
        CComPtr<IStream> pStream;

        // Load the file to a memory stream
        HRESULT hr = FileToStream(szFile, &pStream);

        if (SUCCEEDED(hr))
        {
            // Decode the picture
            hr = ::OleLoadPicture(
                    pStream,            // [in] Pointer to the stream that contains picture's data
                    0,                    // [in] Number of bytes read from the stream (0 == entire)
                    true,                // [in] Loose original format if true
                    IID_IPicture,        // [in] Requested interface
                    (void**)&m_pPicture // [out] IPictire object on success
                    );
        }

        return hr;
    }

    HRESULT DrawImg(HDC hdc, const RECT& rcBounds)
    {
        if (m_pPicture)
        {
            // Get the width and the height of the picture
            long hmWidth = 0, hmHeight = 0;
            m_pPicture->get_Width(&hmWidth);
            m_pPicture->get_Height(&hmHeight);

            // Convert himetric to pixels
            int nWidth    = MulDiv(hmWidth, ::GetDeviceCaps(hdc, LOGPIXELSX), HIMETRIC_INCH);
            int nHeight    = MulDiv(hmHeight, ::GetDeviceCaps(hdc, LOGPIXELSY), HIMETRIC_INCH);

            // Display the picture using IPicture::Render
            return m_pPicture->Render(
                hdc,         // [in] Handle of device context on which to render the image
                rcBounds.left,  // [in] Horizontal position of image in hdc
                rcBounds.top, // [in] Vertical position of image in hdc
                rcBounds.right - rcBounds.left,  // [in] Horizontal dimension of destination rect.
                rcBounds.bottom - rcBounds.top, // [in] Vertical dimension of destination rect.
                0,             // [in] Horizontal offset in source picture
                hmHeight,   // [in] Vertical offset in source picture
                hmWidth,  // [in] Amount to copy horizontally in source picture
                -hmHeight,   // [in] Amount to copy vertically in source picture
                &rcBounds   // [in, optional] Pointer to position of destination for a metafile hdc
                );
        }

        return E_UNEXPECTED;
    }

Достоинства: правильно работает с прозрачными картинками.

Недостатки: не поддерживает анимированный GIF (см. также CPicturEx). Не поддерживает PNG.

Способ 2 (GDI+)

Недостаток ::LoadImage() с лихвой исправили в GDI+. Объект Gdiplus::Image умеет загружать картинки в формате BMP, GIF, JPEG, PNG, TIFF, EXIF, WMF, и EMF:

#include <gdiplus.h>

    HRESULT Load(LPCTSTR szFile)
    {
        USES_CONVERSION;
        // Create new Gdiplus::Image object
        m_pImage = new Gdiplus::Image(T2CW(szFile));
        ATLASSERT(m_pImage);

        // Check for success
        if (Gdiplus::Ok == m_pImage->GetLastStatus())
            return S_OK;

        // Cleanup on failure
        Destroy();
        return E_FAIL;
    }

    HRESULT DrawImg(HDC hdc, RECT& rcBounds)
    {
        if (m_pImage)
        {
            // Create Gdiplus::Graphics object from HDC
            Gdiplus::Graphics graphics(hdc);
            // Create Gdiplus::Rect object from RECT
            Gdiplus::Rect rc(rcBounds.left, rcBounds.top, rcBounds.right, rcBounds.bottom);

            // Draw the image
            return Gdiplus::Ok == graphics.DrawImage(
                m_pImage,                        // [in] Gdiplus::Image object
                rc                                // [in] Position and dimensions
                ) ? S_OK : E_FAIL;
        }

        return E_UNEXPECTED;
    }

Достоинства: понимает множество форматов, в том числе анимированный GIF, правильно работает с прозрачными картинками.

Недостатки: На сегодняшний момент реализован только в WindowsXP. Хотя простое копирование gdiplus.dll в system32 делает ее доступной, как минимум, в Windows2000. Скорее всего, в обозримом будущем ожидаются версии и для Win9x.

Способ 3 (IImgCtx)

Не так давно Майкрософт предоставила заголовочные и библиотечные файлы к объекту ImgCtx, появившемуся еще в Internet Explorer 4.0. Он умеет заргужать картинки в формате BMP, GIF, JPEG, ICO, WMF, EMF, PNG, XBM, ICO, TIFF и, возможно, некоторых других:

#include <IImgCtx.h>

    HRESULT Load(LPCTSTR szFile)
    {
        // Create IImgCtx object
        HRESULT hr = ::CoCreateInstance(CLSID_IImgCtx, NULL, CLSCTX_ALL, IID_IImgCtx, (void**)&m_pImage);
        if (SUCCEEDED(hr))
        {
            // Load URL
            USES_CONVERSION;
            hr = m_pImage->Load(
                    T2COLE(szFile),            // [in] URL
                    0                        // [in] Flags and preffered color format
                    );
        }

        return hr;
    }

    HRESULT DrawImg(HDC hdc, RECT& rcBounds)
    {
        if (m_pImage)
        {
            // Check download state
            DWORD dwState = 0;
            HRESULT hr = m_pImage->GetStateInfo(&dwState, NULL, true);
            if (SUCCEEDED(hr))
            {
                if (IMGLOAD_LOADING & dwState)
                {
                    // Still loading - wait 50 msec and request again
                    ::DrawText(hdc, _T("Loading, please wait..."), -1, &rcBounds, DT_SINGLELINE);
                    ::Sleep(50);
                    Invalidate(false);

                    hr = S_FALSE;
                }
                else if (IMGLOAD_COMPLETE & dwState)
                {
                    // Download successfully complete
                    hr = m_pImage->Draw(
                        hdc,                // [in] Handle of device context on which to render the image
                        &rcBounds            // [in] Position and dimensions
                        );
                }
                else
                {
                    // Download failed
                    hr = E_UNEXPECTED;
                }
            }
            return hr;
        }

        return E_UNEXPECTED;
    }

Достоинства: правильно работает с прозрачными и анимированными картинками. Понимает URL (даже res:// и sysimage://).

Недостатки: не поддерживает загрузку из IStream. Не умеет загружать файлы синхронно.

ПРИМЕЧАНИЕ
Форматов, распознаваемых этим объектом, может быть меньше, например, если при установке IE4 позьзователь отключил поддержку PNG файлов.
Способ 4 (DirectXTransform)

Не смотря на название, эта технология не имеет ничего общего с DirectX. Зато является частью Internet Explorer, внутри которого даже имется набор простеньких классов, реализующих IDirectDraw для нужд DirectXTransform. Этот способ поддерживает тот же набор форматов, что и предыдущий, более того, для этого используется один и тот же код. Разве что синхронно и на выходе получается IDXSurface объект.

    #include <dxtrans.h>

    HRESULT DrawImg(HDC hdc, const RECT& rcBounds)
    {
        if (m_pDCLock)
        {
            HDC hdcImage = m_pDCLock->GetDC();

            // Get the bitmap
            HGDIOBJ hObj = ::GetCurrentObject(hdcImage, OBJ_BITMAP);
            BITMAP bm = {0};

            // Get the size of the bitmap
            if (hObj && ::GetObject(hObj, sizeof(BITMAP), &bm))
            {
                // Draw the image
                return ::StretchBlt(
                    hdc,
                    rcBounds.left, rcBounds.top,
                    rcBounds.right - rcBounds.left,
                    rcBounds.bottom - rcBounds.top,
                    hdcImage,
                    0, 0,
                    bm.bmWidth, bm.bmHeight,
                    SRCCOPY
                    ) ? S_OK : E_FAIL;
            }
        }

        return E_UNEXPECTED;
    }

    HRESULT Load(LPCTSTR szFile)
    {
        CComPtr<IDXTransformFactory> pTransFact;
        CComPtr<IDXSurfaceFactory> pSurfFact;

        // Create the Transform Factory.
        HRESULT hr = ::CoCreateInstance(CLSID_DXTransformFactory, NULL,
            CLSCTX_INPROC, IID_IDXTransformFactory,    (void **)&pTransFact);

        if (SUCCEEDED(hr))
            hr = pTransFact->QueryService(SID_SDXSurfaceFactory,
                    IID_IDXSurfaceFactory, (void **)&pSurfFact);

        if (SUCCEEDED(hr))
        {
            CComBSTR bstrFile(szFile);
            CComPtr<IDXSurface> pDXSurf;

            // Load DX surface.
            hr = pSurfFact->LoadImage(bstrFile, NULL, NULL,
                NULL, IID_IDXSurface, (void**)&pDXSurf);

            if (SUCCEEDED(hr))
            {
                // Get IDXDCLock object
                hr = pDXSurf->LockSurfaceDC(NULL, INFINITE, DXLOCKF_READ, &m_pDCLock);
            }
        }
        return hr;
    }

Достоинства: Прост в использовании. Поддерживает загрузку из IStream.

Недостатки: Медленный и ресурсоемкий. Это связянно с тем, что сначала для картинки создается обертка в виде IDirectDrawSurface, а затем еще одна для IDXSurface, которые нам совершенно не нужны.

Способ 5 (Фильтры импорта)

Многие программы (например PaintBrush или WinWord) при инсталляции кладут в каталог %ProgramFiles%\Common Files\Microsoft Shared\Grphflt некоторое количество файлов, предназначенных для чтения файлов картинок. Способ не документированный и сильно устаревший. Полный список установленных в системе фильтров находится в реестре по адресу SOFTWARE\\Microsoft\\Shared Tools\\Graphics Filters\\Import

Я не буду рассматривать этот способ подробно, поскольку он сильно устарел и очень неудобен. Тем не менее, в приложении DrawImg этот способ реализован наравне с другими.

Способ 6 (Снова Фильтры импорта)

Майкрософт Офис, начиная с версии 8.0 (97) использует новый API с теми же фильтрами.

    HRESULT Load(LPCTSTR szFile)
    {
        HMODULE hModule = g_pMapExtToFilter->LoadFilter(szFile);

        if (NULL == hModule)
            return E_FAIL;

        struct NameStruct
        {
            DWORD    dwHead[2];
            char    szName[MAX_PATH];
            DWORD    dwTail[2];
        };

        typedef DWORD (__stdcall *GetFilterInfo_t)
 (DWORD dwVersion, DWORD dwReserved, HGLOBAL *phFilterData, DWORD dwReserved2);
        typedef DWORD (__stdcall *SetFilterPref_t)
 (HGLOBAL hFilterData, LPCSTR szOption, LPCSTR szValue, DWORD dwReserved2, DWORD dwReserved1);
        typedef DWORD (__stdcall *ImportGr_t)
 (DWORD dwReserved, NameStruct *pFile, ImgInfo *pInfo, HGLOBAL hFilterData);

        GetFilterInfo_t pGetFilterInfo = (GetFilterInfo_t)::GetProcAddress(hModule, "GetFilterInfo");
        SetFilterPref_t pSetFilterPref = (SetFilterPref_t)::GetProcAddress(hModule, "SetFilterPref");

        ImportGr_t pImportGr = (ImportGr_t)::GetProcAddress(hModule, "ImportGr");
        if (NULL == pImportGr)
            pImportGr = (ImportGr_t)::GetProcAddress(hModule, "ImportGR");

        if (pImportGr)
        {
            NameStruct name = {0};
            HGLOBAL hFilterData = NULL;

            if (pGetFilterInfo)
            {
                DWORD dwVer = pGetFilterInfo(2, 0, &hFilterData, 0x00170000);
                ATLASSERT(2 == dwVer);

                if (2 != dwVer)
                {
                    ::FreeLibrary(hModule);
                    return E_UNEXPECTED;
                }
            }

            //    PB 01/26/2001 Turn off dialogs
            if (pSetFilterPref)
            {
                pSetFilterPref(hFilterData, "ShowProgressDialog", "No", 2, 1);
                pSetFilterPref(hFilterData, "ShowOptionsDialog", "No", 2, 1);
            }

            USES_CONVERSION;
            ::lstrcpynA(name.szName, T2CA(szFile), MAX_PATH);

            DWORD dwRet = pImportGr(0, &name, &m_Image, hFilterData);

            if (hFilterData)
                ::GlobalFree(hFilterData);

            if (0 != dwRet || NULL == m_Image.hObj)
            {
                ::FreeLibrary(hModule);
                return E_FAIL;
            }

            if (OBJ_METAFILE != ::GetObjectType(m_Image.hObj))
            {
                HGLOBAL hObj = (HGLOBAL)m_Image.hObj;
                LPBYTE pObj = (LPBYTE)::GlobalLock(hObj);

                m_Image.hObj = ::SetMetaFileBitsEx(::GlobalSize(hObj), pObj);

                ::GlobalUnlock(hObj);
                ::GlobalFree(hObj);
            }

            if (NULL == m_Image.hObj)
            {
                ::FreeLibrary(hModule);
                return E_FAIL;
            }

            return S_OK;
        }

        ::FreeLibrary(hModule);
        return E_UNEXPECTED;
    }

    HRESULT DrawImg(HDC hdc, const RECT& rcBounds)
    {
        if (m_Image.hObj)
        {
            ::SetMapMode(hdc, MM_ANISOTROPIC);
            ::SetViewportExtEx(hdc,
                rcBounds.right - rcBounds.left,
                rcBounds.bottom - rcBounds.top,
                NULL);
            ::PlayMetaFile(hdc, m_Image.hObj);

            return S_OK;
        }

        return E_UNEXPECTED;
    }

Достоинства: понимает очень редкие форматы. Например wpg или cdr

Недостатки: Нет никакой гарантии, что на компьютере пользователя будет установлен нужный фильтр.

Способ 7 (Direct3D)

Direct3D версии 8.0 и выше умеет загружать картинки в формате BMP, JPEG, PNG:

#include <d3dx8.h>
HRESULT hr = ::D3DXCreateTextureFromFile(m_pD3DDevice, szFile, &ppTexture);

Достоинства: если вы разрабатываете 3D-приложение, то это наиболее удобный способ создания текстур (D3DXCreateTextureFromFile автоматически создает необходимое количество MipMap уровней).

Недостатки: если вы не разрабатываете 3D-приложение, то этот способ крайне неудобен, так как предназначен для работы с 3D объектами. На входе нужен IDirect3DDevice8 объект, а на выходе получаем IDirect3DTexture8, который очень не просто вывести в HDC.

Не реализован в демонстрационном приложении.

Это все на сегодня. Пока!

Алекс Jenter jenter@rsdn.ru
Duisburg, 2001. Публикуемые в рассылке материалы принадлежат сайту RSDN.


ПРОГРАММИРОВАНИЕ НА V I S U A L C + +
РАССЫЛКА САЙТА
RSDN.RU
Выпуск No. 60 от 20 января 2002 г.

РАССЫЛКА ЯВЛЯЕТСЯ ЧАСТЬЮ ПРОЕКТА RSDN , НА САЙТЕ КОТОРОГО ВСЕГДА МОЖНО НАЙТИ ВСЮ НЕОБХОДИМУЮ РАЗРАБОТЧИКУ ИНФОРМАЦИЮ, СТАТЬИ, ФОРУМЫ, РЕСУРСЫ, ПОЛНЫЙ АРХИВ ПРЕДЫДУЩИХ ВЫПУСКОВ РАССЫЛКИ И МНОГОЕ ДРУГОЕ.

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

Рад снова вас приветствовать на страницах рассылки.

Эх, меняются времена. Сейчас объем статей становится настолько большим, что они редко просто влезают в ограниченный 60 килобайтами выпуск без разбивания на две части. А когда все-таки влезают, то ни для чего другого места больше не остается ;-) Но я думаю, хорошие статьи все-таки для читателей важнее. Сегодня я предлагаю вам познакомиться с очередной концепцией новой платформы .NET - т.н. сборками.

CТАТЬЯ

Немного о сборках
Автор: Алексей Дубовцев

Source.zip - 1.3 KB

Вводные положения

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

Немного истории

Все началось очень давно, когда Microsoft еще только задумывалась над идеей повторного использования кода. В те времена для решения данной проблемы были созданы динамически подгружаемые библиотеки (Dynamic-Link Load Library, DLL). Они позволяли "выносить" часто используемый код в отдельные библиотеки, которые могли использовать любые приложения. Проблема была в том, что DLL изначально не предоставляли никаких средств управлениям версиями, тогда об этом просто никто не задумывался. Впоследствии Microsoft ввела Version Info (информацию о версии), которая помещалась в DLL как ресурс. Но это не решало проблему полностью, а лишь позволяло определять версию библиотеки, то есть отчасти гарантировало, что будет использоваться именно нужная библиотека. И никак не разъясняло вопрос о том, что делать, если версия библиотеки не совпадает с требуемой. К тому же, вся поддержка версий ложилась на плечи программиста. Для того чтобы корректно поддерживать старые версии, приходилось в уже новых библиотеках оставлять все старые функции, которые и поныне используются старыми приложениями. К примеру, в так называемом ядре Windows - библиотеке Kernel32.dll (которая, по сути, ядром-то и не является), присутствуют многие устаревшие функции, которые в настоящее время используются только устаревшими приложениями, но оставлены для совместимости (WinExec и др.).

ПРИМЕЧАНИЕ
Вы сможете найти список всех таких функций в Platform SDK на странице Obsolete Windows Programming Elements.

Из-за таких функций в последствии сильно затрудняется разработка, так как их количество растет, подобно снежному кому, от версии к версии. Причём не все разработчики добросовестно поддерживают старые версии, чего, кстати, нельзя сказать о Microsoft. Из-за этого и возникают проблемы, похожие на ту, что описана в начале статьи. Представьте, что старая библиотека будет заменена новой, пусть даже и лучшей, но не поддерживающей старые функции. Старое приложение, рассчитанное на старую версию библиотеки, не найдя нужных ему функций, попросту завершит свою работу (собственно и не начиная её).

Вторым шагом было создание COM, который уже предусматривал поддержу версий как неотъемлемую свою часть. Вся поддержка версий в COM в основном основывалась на регистрации приложений и компонентов COM в системном реестре. Что, как позже выяснилось, было далеко не гениальным решением. Поддержка совместимости версий все еще была возложена на плечи разработчика, что само по себе не подразумевало решения проблемы. Ведь что взбредет в голову простому разработчику - одному господу богу известно. Захочет он поддерживать старые версии, или нет, - это его личный выбор. От которого, кстати, будут очень зависеть пользователи. Кроме того, все эти записи о версиях в реестре оказались достаточно неэффективными, так как на практике не были защищены от внешних воздействий (скажем, от установки нового приложения). Порой эти записи повреждались, что было фатальным, и приходилось заново переустанавливать приложения, так и не зная реальной причины их краха.

При проектировании .NET была поставлена задача разработать технологию, которая позволила бы решить проблему версий, быстрого развёртывания и изоляции приложений. В основу новой технологии легли сборки (Assembly), которые призваны решить обозначенные выше проблемы.

Что же это такое - сборки (Assembly)?
  • Сборки - это наименьшие строительные блоки, на которых базируется платформа .NET.
  • Различия в версиях могут существовать только на уровне сборок; предполагается, что внутри сборки никакие элементы (классы, интерфейсы и т. п.) не могут иметь собственные версии.
  • Сборки являются хранилищами как для кода, так и для ресурсов.
  • Сборки самоописываемы - они содержат метаданные (metadata), которые несут в себе информацию о версии, зависимостях, типах, атрибутах и многое другое.
  • Сборки защищены - система защиты исполняемого кода использует права запуска индивидуально для каждой сборки. Автором сборки в метаданных записываются права на использование данной сборки кем бы то ни было, что позволяет защищать код "родными" для системы методами, не прибегая к продуктам сторонних производителей.
Начнем с манифеста

Манифест - это метаданные, включающие информацию о сборке, а именно:

  • Данные о версии - версию, имя и необязательные данные.
  • Список файлов - имена файлов, составляющих сборку, а также их контрольные суммы, вычисляющиеся при помощи криптографических хэш-функций во время создания сборки. Во время выполнения данные файлы проверяются по контрольным суммам, чтобы удостоверится в целостности данного файла, а так же в том, что файл не был подменён другим с таким же именем или просто его новой версией.
  • Зависимости от других сборок - имена и версии сборок, которые используются данной сборкой. Во время выполнения версии сборок строго сверяются, чтобы удостовериться в том, что загружена именно нужная сборка.
  • Экспортируемые типы и ресурсы. Видимость для этих объектов может быть двух типов: только для моей сборки (internal) и для всех (public), включая внешние запросы.
  • Свойства защиты. Здесь можно выделить три типа:
    • Права на запуск данной сборки.
    • Некоторые возможности сборки будут недоступны, если она не лицензирована.
    • Сборка должна запускаться только в том случае, если она лицензирована.
ПРИМЕЧАНИЕ
Список файлов, из которых состоит сборка, и зависимости от других сборок - это совершенно разные вещи. Сборка сама по себе может быть разбита на несколько файлов, хотя для тех, кто ее использует, она будет выглядеть как единое целое. То есть, к примеру, общие классы могут лежать в одном файле, ресурсы - в другом, специальные классы - в третьем файле и так далее. Для чего, спрашивается, это нужно? Во-первых, это нужно для гибкой загрузки распределенных приложений, так как файлы, составляющие сборку, могут загружаться по мере необходимости, а не все сразу. Во-вторых, для создания распределённых приложений, так как местоположение файлов не играет никакой роли: файлы из одной и той же сборки могут находиться где угодно: в Интернете, на сетевых дисках и так далее.
Настало время "поработать руками"

Для начала проверьте, правильно ли у вас настроены пути к Visual Studio.Net. Чтобы правильно настроить пути, вам всего лишь необходимо вызывать при загрузке (ну или как вам нравиться) файл vsvars32.bat, который расположен в директории ...Microsoft Visual Studio.NET\Common7\Tools\.

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

  • Visual Basic.NET

'File:   Some.vb
'Author: Copyright (C) 2001 Dubovcev Aleksey

Imports System

Public Class App

  Public Shared Sub Main()

    Console.WriteLine("Hello World")

  End Sub

End Class
  • C#

/*
File:   Some.cs
Author: Copyright (C) 2001 Dubovcev Aleksey
*/

using System;

public class Application
{
  public static void Main()
  {
    Console.WriteLine("Hello World");
  }
}
  • Managed Visual C++

/*
File:   Some.cpp
Author: Copyright (C) 2001 Dubovcev Aleksey
*/

#using <mscorlib.dll>

using namespace System;

void main()
{
  Console::WriteLine("Hello World");
}

Теперь, когда вы построили exe файл, запускайте утилиту ildasm.exe (Intermediate Language Disassembler - дизассемблер промежуточного языка) следующим образом:

ildasm.exe /adv HelloWorld.exe

Параметр командной строки /adv откроет дополнительные пункты меню, которые понадобятся нам позднее. Полную информацию о данной утилите вы сможете найти в .NET Framework Sdk.

Рис. 1

Вы должны увидеть то же самое, что и на рисунке. Древовидная структура (далее просто дерево) показывает вам сборку изнутри.

[...]

ПРИМЕЧАНИЕ
Тип по значению (Value Type) задается ключевым словом struct и отличается от класса тем, что размещается в стеке, а не в динамической памяти.

Поэкспериментируйте немного с ildasm, чтобы привыкнуть к этой программе. Не пугайтесь при виде каких ни будь непонятных данных, дальше будет еще страшнее. :)

Теперь откройте манифест (manifest) и внимательно посмотрите. Ниже я привожу содержание манифеста, полученное мной при помощи утилиты ildasm.

//  Microsoft (R) .NET Framework IL Disassembler.  Version 1.0.2914.16
//  Copyright (C) Microsoft Corp. 1998-2001. All rights reserved.

// VTableFixup Directory:
// No data.

//Это ссылка на основную библиотеку классов .NET
.assembly extern mscorlib
{
  //Это хеш публичного ключа данной сборки
  //он нужен для подтверждения валидности сборки
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         // .z\V.4..
  //Версия сборки которая использовалась при создании приложения
  .ver 1:0:2411:0
}

//Описание нашей сборки
.assembly Some
{
  // --- The following custom attribute is added automatically, do not uncomment -------
  // --- Следующий атрибут добавлен автоматически, не убирайте комментарий
  //  .custom instance void [mscorlib]System.Diagnostics.DebuggableAttribute::.ctor(bool,
  //                                                                                bool) = ( 01 00 00 01 00 00 )

  //Алгоритм по которому считается хэш
  .hash algorithm 0x00008004
  //Версия нашей сборки
  .ver 0:0:0:0
}

//Название запускаемого файла
.module Some.exe
// MVID: {2FA89A98-AD9F-4E31-8DB1-AB1FFB64A4F4}

//Предпочтительный адрес для загрузки сборки
.imagebase 0x00400000

//Подсистема (консоль, оконное приложение, приложение времени загрузки)
.subsystem 0x00000003

//Выравнивание секций
.file alignment 512

//Зарезервированный флаг
.corflags 0x00000001

Что, вам кажется, что это полная чушь? Ошибаетесь, если в этом разобраться, что, кстати, не так уж и трудно, то вам откроется много очень полезной и порой необходимой информации. В начале вы увидите записи со словами .assembly extern, которые описывают зависимости от внешних сборок, необходимых для функционирования этой программы. А данные, идущие далее в блоке, заключённом в фигурных скобках, описывают версию и контрольную сумму сборки. Эти данные берутся из сборок при компиляции программы, что гарантирует использование именно тех сборок, которые использовались при компиляции и тестировании. Далее следует .assembly, но уже без модификатора extern. С этой директивы и начинается описание нашей с вами сборки. Как вы могли догадаться, .ver описывает версию нашей сборки. Ну а .hash algorithm определяет функцию, по которой будет вычисляться хэш, но об этом я расскажу позднее. Затем идут описания имени самого модуля, подсистемы исполнения, информация о выравнивании секций и еще некоторые данные. Полная документация по этому вопросу находится в Framework SDK. Более подробно об устройстве манифеста я расскажу далее.

ПРИМЕЧАНИЕ
На самом деле, .publickeytoken описывает не контрольную сумму файла, а является хэшем (контрольной суммой) публичного ключа автора, создавшего сборку, на которую ссылается ключ .assembly extern.
Давайте "копнём" поглубже

Как же сборка устроена изнутри? Что у нее "под капотом"? Оказывается, не так все и страшно, как вам могло показаться. Сборка помещается внутри файла в формате PE (Portable Executable), то есть внутри DLL или EXE. Здесь все зависит от того, будет ли сборка самостоятельной программой или "библиотекой". Любая сборка импортирует функции из библиотеки mscoree.dll, которая является частью среды исполнения. Исполняемые файлы (EXE) импортируют из этой библиотеки функцию _CorExeMain, которую они вызывают для своего запуска. А происходит это так: как и в любом exe-файле, в нашем присутствует точка входа - это маленькая функция (6 байт), которая призвана передавать управление функции _CorExeMain из библиотеки mscoree.dll. Когда данная функция получает управление, она находит в exe-файле свою точку входа и начинает выполнение с нее. Вы можете проверить все сказанное мною сами при помощи утилиты dumpbin, запустив ее с параметрам /imports. Правда, у файла, скомпилированного на Managed C++, вы можете увидеть много других импортов. Не пугайтесь, это нормально, так как MC++ одновременно поддерживает как управляемые, так и неуправляемые данные (managed/unmanaged data). А значит, может делать самостоятельные системные вызовы в обход CLR.

Динамически загружаемые библиотеки импортируют функцию _CorDllMain, которую они вызывают из DllMain, "точки входа" DLL. Сама же функция точки входа ничего, кроме вызова _CorDllMain, не делает. С exe-сборками дело обстоит аналогичным образом, только вместо _CorDllMain, импортируется функция _CorExeMain.

Вот как выглядят точки входа в dll- в exe-сборки.

Dll сборка
Exe сборка

start   proc near
  jmp ds:_CorDllMain
start   endp

start   proc near
    jmp ds:_CorExeMain
start   endp

В будущих версиях Windows Microsoft планирует изменить процесс загрузки, при котором реальная точка входа будет находиться не таким странноватым способом. Вы спросите: а почему нельзя было сделать сразу, нормально? Это на самом деле не такой простой вопрос, как кажется. Ведь новые исполняемые файлы, по сути дела, со старыми ничего общего не имеют, а надо сделать так, чтобы они могли запускаться существующими версиями Windows без изменения загрузчика файлов. Microsoft пошла по давно известному пути "оборачивание нового в старое", то есть сам exe-файл является оберткой, для реального исполняемого файла CLR. Благодаря этому подходу файлы могут запускаться на уже существующих версиях Windows без изменения их самих.

Сборка может состоять из следующих частей: манифест - метаданные, описывающие сборку, описание типов, код в виде промежуточного языка (IL) и набор ресурсов. Не все из них должны обязательно присутствовать в каждой сборке, кроме, разумеется, манифеста. Сборка также может быть разбита на несколько файлов. Для большей наглядности я нарисовал, как это все происходит.

Рис. 2

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

Управлениями версиями и изоляция
Немного лирики

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

Концепция сборок предоставляет полную изолированность приложений (ну, почти полную). Сборки делятся на два типа:

  • приватные (private) - те которые используются только самим приложение
  • совместные (shared) - те которые используются всеми.

Приватные сборки поставляются с самим приложением, используются только им и храниться в его папке. Microsoft рекомендует использовать совместные сборки только при крайней необходимости. Да, вы, конечно, можете сказать, что это нецелесообразно - с каждым приложение поставлять одни и те же сборки, можно ведь сделать сборку совместной, сэкономив тем самым немного места. Но на самом деле теперь, когда цена за мегабайт дискового пространства неуклонно уменьшается, а объем дисков неуклонно увеличивается, проблемы объема, занимаемого вашим приложением, не должны вас беспокоить. Собственно, здесь нужно выбирать между устойчивостью и небольшим увеличением эффективности в виде сохранения дискового пространства. Я лично думаю, что надо остановиться на первом. Хотя эта проблема может показаться для вас незначительной, на самом деле все очень серьёзно. Вдумайтесь: над решением этой проблемы компания Microsoft работает с выхода первых версий Windows и по сей день. И только сейчас в .NET были предложены четко стандартизированные и хорошо продуманные средства для решения этой проблемы. Хотя, как знать? Все еще может обернуться провалом, как это бывало и раньше.

Приватные сборки

Приватные сборки видны только самому приложению и никому более, то есть приложение изолируется от внешнего воздействия как других программ, так и самой операционной системы. Соответственно, приватные сборки лишены многих проблем, связанных с совместными сборками. К примеру, такой, как уникальность имен: так как сборка приватна, нет необходимости заботится об уникальности имен во всем глобальном пространстве имен. Концепция приватных сборок сильно упрощает развёртывание (инсталляцию) ваших приложений, так как больше не придется делать записей в реестре, подобных тем, которые вы делали ранее для регистрации ваших COM-компонентов. Теперь вы будете просто копировать ваши сборки в директорию вашего приложения или в подчиненные директории. Общая среда исполнения (CLR) при запуске вашего приложения прочитает его манифест и определит, какие сборки необходимы. Затем будет произведён процесс зондирования (probing) (звучит прямо как зомбирование :) директории вашего приложения на предмет нужной сборки, сборка соответственно определяется по имени файла, определенного в манифесте.

ПРИМЕЧАНИЕ
Для справки: процесс зондирования (или зомбирования - это как кому больше нравится) - это просто рекурсивный поиск в директории вашего приложения (то есть при поиске сборки просматриваются все поддиректории).

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

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

Совместные сборки

Среда исполнения .NET поддерживает также совместные сборки. Это сборки, которые могут быть использованы сразу несколькими приложениями и которые, соответственно, "видны" всем. Правда, к таким сборкам предъявляются более строгие правила, к приватным сборкам. Например, необходима уникальность имен сборки: имена внутри сборки не должны конфликтовать с уже существующими в глобальном пространстве имен, предоставляемом средой исполнения по умолчанию, хотя система и предоставляет сервисы защиты имен (protection of the name). Специально для реализации этого сервиса была разработана технология Shared Names (совместные имена), описываемая далее.

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

ПРИМЕЧАНИЕ
Тут может возникнуть вопрос: так где же она, изолированность? Ведь я тут вроде "распинался" о полной изолированности, можно сказать кричал, что все так круто, и вот тебе раз, оказывается, любой, кому нужно, сможет подменить версию, выглядит вроде нелогично, а может быть даже и абсурдно. Но давайте взглянем более детально, ведь политику надо задавать для определенного приложения с учетом его специфики, то есть просто как случайно изменить политику версий не удастся. Вот, вроде, всё и стало на свои места.

Совместные сборки хранятся в глобальном кеше сборок (global assembly cache - GAC). Сборки, хранящиеся там, используются многими приложениями. К ним также имеет доступ администратор, который при необходимости сможет ставить патчи (Udgrade) на совместно используемые сборки, которые, соответственно, окажут влияние на все приложения, которые используют данные сборки. Для примера это может быть какая-нибудь заплатка на общие библиотеки среды, к примеру исправление в каком-нибудь классе. Реально хранилище сборок располагается в директории WinNt\assembly. Если вы ее будете просматривать при помощи проводника, вы увидите что-нибудь похожее на приведённый ниже рисунок.

Рис. 3

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

Рис. 4

Структура этих директорий хотя и может показаться с первого взгляда очевидной, но на самом деле она очень хорошо продумана и спроектирована. Как вы уже могли заметить, в папке GAC есть подпапки, представляющие каждую сборку. Можно было бы подумать, что в них и будут храниться файлы с самими сборками, ан нет. Разработчики GAC поступили куда дальновиднее, в каждой папке, представляющей сборку хранятся подпапки, разбивающие данную сборку по версиям. Таким образом, у нас может храниться любое количество версий одной и той же сборки. Зачем же, спрашивается, это нужно? Ведь такой подход никак ни способствует экономии места на диске. Все на самом деле проще, чем вы могли подумать. Как я уже говорил ранее, Microsoft стремилась сделать приложения как можно более стабильными и даже пошла ради этого на жертву в виде вашего дискового пространства. При таком подходе каждое приложение сможет использовать именно ту совместную сборку, на которую оно рассчитано. То есть при загрузке приложения для него будет выбираться сборка именно той версии, которую он запросит, хотя может существовать и гораздо более новая версия этой же сборки.

ПРИМЕЧАНИЕ
Хотя здесь надо оговориться: к любому приложению может быть применена политика версий, которая принудительно его заставит использовать указанную в политике сборку.

Поиск в глобальном хранилище сборок идет, основываясь на строгих сведениях о версии, что позволяет избегать многих проблем, описанных ранее. Для повышения надежности и стойкости системы GAC, вы не сможете производить какие либо действия (кроме, конечно, простого просмотра) с глобальным хранилищем, если вы не будете иметь прав администратора. Такая политика введена по умолчанию в отношении GAC, хотя при желании ее можно будет изменить. А что это вообще означает? Тут, на самом деле, все интереснее, чем могло бы показаться с первого взгляда. Смысл данной политики таков: пользователь, не имеющий прав администратора, не сможет как-либо повлиять на работу приложений, установленных в системе, хотя и сможет устанавливать приложения, но лишь те, которые не будут использовать совместных сборок. Иными словами, ничего глобального простому пользователю испортить не удастся.

Как "работает" информация о версиях

Сама версия состоит из четырёх чисел; для наглядности, пожалуй, даже нарисую.

Рис. 5

На практике это выглядит, к примеру, так: 1.0.2.3. Где:

  • Major - основная версия.
  • Minor - подверсия приложения.
  • Build - количество построений (полных компиляций) для данной версии.
  • Revision - Номер ревизии для текущего построения.

В версии выделяется основная часть и дополнительная. При поиске нужной сборки, основная часть версии должна строго совпадать, а с дополнительной частью всё происходит хитрее. Если будет найдено несколько сборок с одинаковыми основными частями, то будет выбрана сборка с наибольшей дополнительной частью. Версию сборки вы можете задать при помощи параметра командной строки либо при помощи атрибута System.Reflection.AssemblyVersionAttribute. Здесь, правда, есть одна хитрость: вы можете задавать версию не полностью, а только ее часть. К примеру вот так: "1.*","1.5.*,@1.5.2.*". При отсутствии каких либо частей, компилятор допишет их сам по следующим правилам:

  • Minor - приравнивается к нулю
  • Build - приравнивается количеству дней прошедших с первого января 2000 года
  • Revision - приравнивается количеству секунд, прошедших с полуночи, деленных на два.

Такая, казалось бы, странная схемы выбрана далеко не случайно: она позволяет гарантировать уникальность версии для каждого построения приложения, что, вообщем-то, немаловажно.

Технологи прямого запуска (Side-By-Side Execution)

Я долго думал, как на словах описать эту технологию, но что-то ничего не лезло в голову, поэтому я решил нарисовать. Думаю, что, посмотрев на рисунок, вы сразу многое поймёте. Главное, обратите внимание на версии.

Рис. 6

Идея состоит в том, что для одного и того же приложения могут быть загружены разные версии одной и той же сборки, и при этом оно ничего не будет знать об этом. Для примера, библиотеки, обозначенные на рисунке цифрами 1 и 2, будут загружены для нашего приложения и может даже будут одновременно работать, но ни одна из них не узнает о существовании другой, что позволит избежать каких-либо конфликтов.

ПРЕДУПРЕЖДЕНИЕ
Не все так хорошо, как могло показаться. Да, система CLR не допустит прямых конфликтов между сборками разных версий, но приложение-то по-прежнему исполняется прежде всего операционной системой, а не CLR. И надо обязательно учитывать тонкости ОС, чтобы избежать проблем. К примеру, мы создаем именований объект ядра с некоторым именем, пускай это будет проецируемый в память файл. Все вроде кажется нормально, но тут подгружается в память другая версия нашей же библиотеки, и создает проецируемый в память файл с таким же именем. И что? А то, что для него не создается новый файл, а открывается уже существующий, созданный ранее другой версией нашей библиотеки. Думаю, вы сами можете себе представить, что при этом может произойти. При проектировании ваших собственных сборок вы должны обязательно помнить о таких, казалось бы, незначительных вещах. Это позволит вам в будущем избежать множества неприятных проблем.
Проверка подлинности

Допустим, что среда выполнения (CLR) при загрузке приложения нашла требуемую сборку с подходящей версией. По идее, надо начать ее загружать, но возникает вопрос: а как узнать, что это действительно именно та сборка, которая нам нужна, а не другая. Ведь возможно, что кто-то захочет выдать свою сборку за чужую, или нужная сборка оказалась поврежденной, скажем, при передачи по сети. COM, к примеру, не предусматривал решения этой проблемы. В .NET для решения данного вопроса используются односторонние хэш-функции, в простонародье называемые контрольными суммами. В дополнении к информации об используемой сборке, во время компиляции считается контрольная сумма используемой сборки и помещается вместе с информацией о версии. Во время загрузки проверяется контрольная сумма, и на основание этой проверки будет вынесет вердикт о валидности сборки. По умолчанию используется хэш-функция SHA1. Плюс еще используется технология подписывания (signing) сборок, основанная на открытых криптографических алгоритмах с парными ключами. Вообще, ребята из Microsoft здорово поработали над этой проблемой. Ими было применено очень много интересных решений, о которых я, возможно, расскажу в отдельной статье.

Развертывание приложений

Для простого развертывания приложений были существенно расширены сервисы, предоставляемые Windows Installer. Теперь он полностью поддерживает .NET. Создание инсталляций станет более простым, так как теперь данная возможность интегрирована в стандартную поставку среды разработки .NET. То есть вы сможете использовать все современные сервисы, предоставляемые Windows Installer, в ваших приложениях, не прибегая к средствам сторонних производителей. А предоставляет он их, кстати, не мало, но об этом в отдельной статье.

Заключение

Хотя вам может показаться, что все, что я рассказал, не особо нужно, но на самом деле это совсем не так. Вы, конечно, можете сказать: "Зачем мне знать какие-то страшные низкоуровневые подробности, когда можно заняться чем-нибудь конкретным, к примеру написать компонент для .NET". Я считаю, что такой подход крайне неверен. Изучая систему сверху, невозможно понять всей сути происходящего в ней, вы сможете лишь заучить некоторые стандартные приемы работы с системой, о которых вы где-либо прочитаете или найдете соответствующий пример. Если же понять систему изнутри, то она откроется для вас с совершенно новой стороны. Вы сможете решать проблемы совершенно неординарным способом, основываясь не на каких-то там примерах, а на фундаментальных знаниях о системе. То, что я осветил в статье, является лишь маленькой верхушкой огромного айсберга .NET. Впоследствии я буду описывать некоторые из технологий, упомянутых выше, более детально, но все равно обо всем написать я никак не смогу при всем своем желании. Прочитав статью, поэкспериментируйте, посмотрите, поизучайте то, что вас заинтересует. Главное - никогда не останавливайтесь на достигнутом.

Это все на сегодня. До скорого!

Алекс Jenter jenter@rsdn.ru
Duisburg, 2001. Публикуемые в рассылке материалы принадлежат сайту RSDN.


ПРОГРАММИРОВАНИЕ НА V I S U A L C + +
РАССЫЛКА САЙТА
RSDN.RU
Выпуск No. 61 от 27 января 2002 г.

РАССЫЛКА ЯВЛЯЕТСЯ ЧАСТЬЮ ПРОЕКТА RSDN , НА САЙТЕ КОТОРОГО ВСЕГДА МОЖНО НАЙТИ ВСЮ НЕОБХОДИМУЮ РАЗРАБОТЧИКУ ИНФОРМАЦИЮ, СТАТЬИ, ФОРУМЫ, РЕСУРСЫ, ПОЛНЫЙ АРХИВ ПРЕДЫДУЩИХ ВЫПУСКОВ РАССЫЛКИ И МНОГОЕ ДРУГОЕ.

Добрый день, дорогие друзья!

НОВОСТИ

Сегодня я чрезвычайно рад сообщить вам отличную новость - появился новый совместный проект сайтов www.rsdn.ru, delphi.mastak.ru и www.optim.ru - профессиональный журнал для программистов RSDN Magazine.

Все его содержание создается профессиональными программистами, и рассчитано на профессиональных программистов. Мы считаем, что материалы журнала должны носить не обзорный, а углубленный характер и быть реально полезны программисту в его повседневной работе. Именно практическая полезность материалов является для нас важнейшим критерием формирования контента издания. Об уровне и характере статей вы можете судить по материалам наших сайтов.

В журнале вы найдёте статьи самой различной тематики, ответы на вопросы, а на прилагаемом к нему компакт-диске - полезные утилиты, компоненты (в форматах ActiveX, Delphi, .Net) и многое другое. Кроме этого, в состав компакт-диска будут включены различные SDK, такие как Platform SDK, .Net SDK и т.п.

Журнал будет также постоянно сотрудничать с фирмами-разработчиками наиболее современных технологий программирования и средств разработки. На сайтах, участвующих в выпуске журнала вы сможете пообщаться с авторами статей, задать коллегам вопросы и поделится с ними мнениями по интересующим Вас темам.

Тематика публикаций журнала будет охватывать:

  • Технологии (COM, Java, .Net, CORBA, DirectX, OpenGL и пр.)
  • Алгоритмы и структуры данных
  • Различные API (Win32, GDI+, и т.п)
  • Методологии организации процесса программирования
  • Инструментальные средства и средства разработки
  • Библиотеки (VCL, MFC, STL, ATL, и т.п.)
  • И разумеется постоянные обзоры новинок и перспективных направлений в IT индустрии

Регулярный выпуск журнала начнется со 2 полугодия 2002 года. Сигнальный номер RSDN Magazine выйдет в свет в 1 квартале 2002 года. Периодичность выхода на начальном этапе - 1 раз в 2 месяца. В дальнейшем планируется переход на ежемесячный выпуск. Примерный объем журнала - около 100 страниц формата A4. Ориентировочная цена номера с компакт-диском - около 100 руб.

Начиная со второй половины 2002 года журнал будет распространяться по подписке через Роспечать и альтернативные агентства распространения. Индекс по каталогу "Роспечать" - 81263.

Сигнальный номер журнала не будет доступен по подписке через Роспечать и альтернативные агентства распространения. Поэтому велика вероятность, что вы не увидите его в продаже. Но все желающие его получить могут заказать журнал прямо сейчас, заполнив соответствующую форму. Журнал будет доставлен вам по почте.

Стоимость журнала с доставкой:

  • по России - 100 руб.
  • в страны СНГ и Балтии- 170 руб.
  • в страны дальнего зарубежья - 250 руб.

Реквизиты, по которым необходимо произвести платеж, вы найдете здесь.

ПРИМЕЧАНИЕ
Единственной проблемой при подписке за пределами РФ является оплата. С доставкой проблем нет. В республиках бывшего СССР обычно можно заплатить через почту или сбербанк. Иностранцам придется (пока) искать собственный путь для оплаты. Например, можно оплатить подписку через знакомых (друзей, родственников) живущих в России.

Мы надеемся, что вас заинтересовало это новое издание. Мы же заинтересованы в том, чтобы сделать журнал как можно более интересным для вас. Направляйте любые предложения по адресу mag@rsdn.ru.

CТАТЬЯ

Публикуемая в этом выпуске статья взята из пре-первого (#0) номера журнала.

Анатомия C Run-Time
или
Как сделать программу немного меньшего размера
Автор: Виталий Брусенцев
Источник: RSDN Magazine #0

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

"Я создал проект с использованием библиотеки ATL. Некоторое время он прекрасно компилировался как в Debug-, так и в Release-версии. Затем, после добавления очередной порции кода, при сборке Release-версии линкер выдал ошибку:

LIBCMT.lib(crt0.obj) : error LNK2001: unresolved external symbol _main

Что делать?"

Иногда на подобный вопрос можно получить следующий ответ:

"Да, у меня тоже была такая ошибка. Вылечилось добавлением в исходники пустой функции main(){}.Это какой-то глюк у Microsoft. :( "

Что же на самом деле стоит за этой проблемой и как ее решить? Давайте разберемся.

Многое в этой статье справедливо для любой среды программирования на C/C++, но детали реализации будут приводиться для Microsoft Visual C++ версий 5.0 и 6.0.

Большое спасибо Павлу Блудову за ценные замечания в ходе обсуждения статьи.

Библиотека C Run-Time

Обычно C/C++-программа опирается на мощную поддержку С Run-Time Library - библиотека времени исполнения языка C, далее - CRT; более редкое название - RTL (Run-Time Library). Многим функциям этой библиотеки для правильной работы требуется дополнительная инициализация (CRT startup code). В частности, для вывода текста на консоль с помощью функции printf необходимо, чтобы дескриптор стандартного вывода stdout был предварительно связан с устройством вывода операционной системы (например, стандартным выводом и консолью Win32). То же самое справедливо и для функций работы с кучей - таких, как malloc для C и оператора new для C++.

Таким образом, даже в минимальной программе, содержащей вызов printf или попытку выделения динамической памяти, будет содержаться внушительный (для такой программы) код инициализации CRT - свыше 30 килобайт.

ПРИМЕЧАНИЕ

При использовании CRT в виде дополнительной динамической библиотеки (DLL) размер исполняемого модуля может быть меньше 30 Кб - об этом речь пойдет чуть позже.

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

Так, некоторые операции с плавающей точкой требуют наличия кода инициализации: например, на случай, если будет выполняться обработчик исключительных ситуаций (floating point handler). Объявление глобальной переменной, являющейся экземпляром класса, имеющего конструктор или деструктор, тоже требует наличия стартового кода CRT. Это происходит из-за того, что вызовы конструкторов и деструкторов в VC реализованы как часть стартового кода CRT. Использование механизмов обработки исключений C++ и Run-Time Type Information (RTTI) также влечет за собой необходимость инициализации.

Исходя из этого, разработчики современных компиляторов C++ строят CRT таким образом, чтобы её стартовый код включался в программу по умолчанию. В большинстве случаев это - именно то поведение, которое требуется. В самом деле, большой проект на C++ редко обходится без использования CRT-функций или вычислений c плавающей точкой. Да и "довесок" в 30 Кб в таком случае невелик.

Если это вас устраивает, проблема с упомянутым ATL-проектом решается достаточно просто. Необходимо зайти в настройки проекта ("Project" - "Settings"), выбрать нужную Release-конфигурацию и на закладке "C++" удалить опцию препроцессора _ATL_MIN_CRT. Вопрос будет снят. Дальше можно не читать.

Но встречаются случаи, когда считаешь буквально каждый байт исполняемого модуля. Это может быть ядро инсталлятора или самораспаковывающегося архива, элемент управления ActiveX, который скачивается через Интернет, или приложение для встраиваемой системы. Компиляторы C++ (и Visual C++, в том числе), на мой взгляд, наиболее подходят для такого рода разработок. Приложение может, в конце концов, состоять из большого количества модулей, и мало что значащие 30 Кб могут превратиться в несколько сотен килобайт, а то и мегабайт. Но для контроля над процессом сборки придется погрузиться в некоторые детали реализации поддержки CRT.

main или WinMain?

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

Чтобы немного развлечься, проведем эксперимент. Создадим файл test.cpp:

#include <windows.h>

int main()
{
    MessageBox(0, "Hello from main()", "A test program", MB_OK);
    return 0;
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
    LPSTR lpCmdLine, int nShowCmd)
{
    MessageBox(0, "Hello from WinMain()", "A test program", MB_OK);
    return 0;
}

Внимание, вопрос: что появится на экране после запуска такой программы? Постарайтесь ответить на этот вопрос, не заглядывая в дальнейшее описание.

ПРИМЕЧАНИЕ

Я не стал рассматривать еще два возможных варианта стартовой функции: wmain или wWinMain, предназначенных для проектов, компилируемых в Unicode. Кроме того, при создании DLL имеется еще один вариант стартовой функции - DllMain.
Точка входа в программу

Функция [w]main или [w]WinMain, с которой начинается выполнение программы, вовсе не является точкой входа исполняемого модуля! На самом деле, программа на C++ начинает работу с выполнения специальной процедуры инициализации. Что касается Win32, то адрес этой процедуры и содержится в поле AddressOfEntryPoint заголовка Portable Executable (PE) выполняемого файла. Она представляет собой обычную функцию C, описанную с соглашением о вызовах __stdcall. В зависимости от настроек проекта, в Visual C++ эта функция может называться [w]mainCRTStartup, [w]WinMainCRTStartup или _DllMainCRTStartup (символ 'w' добавляется к имени для Unicode-проектов). Конкретно же для сборки приложения имя функции-точки входа можно задать опцией линкера /entry. Умолчанием для Visual C++ является "mainCRTStartup". Все сказанное справедливо и для некоторых других компиляторов C++ для Win32.

Что же происходит во время ее выполнения? Вот типичный сценарий работы такой функции (случай DLL здесь не рассматривается).

  • Инициализируются переменные CRT (такие, как errno и osver). Многопоточная библиотека требует особой инициализации.
  • Происходит инициализация динамической памяти (кучи).
  • Инициализируется среда обработки ошибок в вычислениях с плавающей точкой. Это необходимо не только для библиотечных функций (таких, как sqrt), но и для преобразований между целочисленными и плавающими типами данных.
  • Получаются значения аргументов командной строки программы и переменных среды.
  • В случае необходимости, происходит инициализация консоли и привязка стандартного вывода к файловым дескрипторам C. При старте исполняемого файла, у которого в уже упомянутом заголовке PE значение поля Subsystem равно 3 (Windows character-mode executable), создается консоль. Это значение можно задать опцией линкера /subsystem. Выбор подсистемы выполнения также влияет на выбор стартовой функции (если ее имя не задано явно). Умолчанием является "console".
  • Происходит вызов цепочки функций инициализации CRT и конструкторов глобальных переменных (подробнее об этом - в следующем разделе).
  • И лишь после этого вызывается функция [w]main или [w]WinMain. Коротко можно сказать, что функция xxxCRTStartup вызывает соответствующую функцию xxx.
  • Программа работает.
  • Выполняется последовательность действий по очистке, к которой мы еще вернемся.
  • И, наконец, происходит завершение процесса.

Теперь, наконец, можно ответить на мой вопрос: он был задан некорректно :). В самом деле, результат сборки будет зависеть от набора опций компоновщика, установленных в проекте или по умолчанию.

Так, например, при вызове компилятора в командной строке таким образом:

cl test.cpp user32.lib

мы получим консольную программу и сообщение "Hello from main()" (вспомните, что говорилось об умолчаниях).

А вызвав компилятор вот так:

cl test.cpp user32.lib /link /entry:WinMainCRTStartup /subsystem:console

мы получим "чудо чудное": программу, у которой выполняется функция WinMain, но создается окно консоли.

Код инициализации глобальных переменных

Как в VC++ реализован вызов цепочки функций инициализации/завершения?

Наличие в программе хотя бы одной глобальной переменной - экземпляра класса - заставляет компилятор сделать следующее. Во-первых, он генерирует невидимую за пределами модуля функцию, в которой и выполняются необходимые действия - вычисляется значение инициализатора или вызывается конструктор. Далее создается специальная запись с указателем на эту функцию в сегменте с именем вида ".CRT$xxx". Детально разбирать формат именования сегмента мы не будем, сейчас важно только то, что все сегменты такого типа будут при сборке объединены в алфавитном порядке в один сегмент. Таким образом, в момент старта программы в памяти будет находиться массив указателей на функции, при вызове которых и произойдут необходимые действия. В стартовом коде CRT VC этим занимается функция _initterm.

А почему здесь используется термин "функции инициализации/завершения " вместо терминов "конструкторы/деструкторы"?

Напомню, что стандарт языка C++ разрешает инициализацию переменных с помощью неконстантных выражений. Если переменная (даже простого типа) описана в глобальной области, то ее инициализатор должен быть выполнен до вызова функции main/WinMain:

int len = strlen("Hello, world!");

Обработка в этом случае ничем не отличается от инициализации экземпляра класса имеющего конструктор.

Код завершения

Упомянув инициализацию CRT, нельзя умолчать о коде очистки, или завершения. В нем выполняются действия обратного характера (и, в том числе, деструкторы глобальных переменных). Что действительно заслуживает описания, так это то, что код очистки можно вызвать собственноручно. Да-да, он содержится в функции exit. Если же не вызвать ее явно, то она вызовется после возврата из main/WinMain. Наиболее выразительную реализацию вышесказанного я встретил однажды в исходных файлах CRT компилятора WATCOM C++:

exit(main(__argv, __argc, __envp));

То есть, можно сказать, что все выполнение программы имеет целью получение параметра для функции exit. :)

ПРИМЕЧАНИЕ

Вообще-то, exit (вернее, возможность ее прямого вызова) является, скорее, "пережитком" со времен программирования на C. При вызове этой функции из программы на C++ не выполнятся деструкторы для локальных переменных (что естественно, поскольку, в отличие от глобальных объектов, их деструкторы нигде не зарегистрированы). Кроме того, вызов exit из деструктора может привести к входу программы в бесконечный цикл, так что не злоупотребляйте этой функцией.

Со времен создания библиотеки языка C осталась и такая возможность, как регистрация цепочки обработчиков завершения с помощью функций atexit/_onexit. Функции, зарегистрированные вызовом atexit/_onexit, будут вызваны в ходе завершения программы в порядке, обратном порядку их регистрации. Для программы на C++ с этой целью лучше воспользоваться глобальными деструкторами.

На самом деле, в программе на VC регистрация деструкторов глобальных объектов также выполняется с помощью внутреннего вызова atexit после вызова конструктора. Это имеет довольно веские основания: если конструктор объекта вызван не был, то не будет вызван и его деструктор. Но, в любом случае, это - деталь реализации, на которую полагаться не стоит.

Внутри exit содержится вызов функции более низкого уровня - _exit. Ее вызов не приведет к вызову деструкторов и exit-обработчиков, а только выполнит самую необходимую очистку (не буду вдаваться в подробности, замечу только, что при этом вызываются C-терминаторы (функции из таблицы в сегментах "CRT$XT[A-Z]"), в частности, подчищается low-level i/o) и завершит программу вызовом функции Windows API ExitProcess.

И, наконец, функция abort является способом "пожарного" завершения программы. Она выводит диагностическое сообщение и также вызывает _exit для завершения процесса.

Вызов любой из этих функций приведет к необходимости включения стартового кода CRT.

Уменьшаем размер выполняемого модуля

Но в нашем примере нет ничего, что потребовало бы использовать CRT. Более того, включив оптимизацию по размеру (/O1) и генерацию карты исполняемого файла (Generate Link Map, /Fm), можно заметить, что размер функции main - всего 23 байта. А размер выполняемого модуля составляет около 36 килобайт. Неужели нельзя его немного уменьшить?

Конечно, такие способы существуют, и некоторые из них я опишу ниже. Но важно понимать, что каждый из них не является общим решением (иначе именно он использовался бы по умолчанию), и имеет свои недостатки.

Использование внешней библиотеки CRT

Откомпилируем нашу программу следующей командой:

cl /MD test.cpp user32.lib

Размер полученного в результате EXE-файла составляет около 16 килобайт. Что за чудеса? Куда делась половина исполняемого модуля? Неужели он "похудел" за счет исключения CRT?

И да, и нет. Опция компилятора /MD указывает использовать для сборки библиотеку MSVCRT.LIB. В ней содержится только тот набор кода, который позволяет линкеру разрешить внешние связи. А сам код CRT находится в динамической библиотеке MSVCRT.DLL в системном каталоге Windows. Эта многопоточная библиотека используется и некоторыми бесплатными компиляторами C/C++ для Windows, например, MinGW.

Такое решение достаточно удобно, если проект состоит из нескольких модулей - каждый из них станет меньше на объем рантайма. Кроме того, оно позволяет Microsoft исправлять ошибки в уже выпущенных программах простой заменой старой DLL на исправленную версию. Этот подход активно используется многими разработчиками, использующими библиотеку MFC: если в опциях проекта выбрать "Use MFC in a shared DLL", то придется использовать динамическую версию CRT, иначе проект попросту не соберется. В интегрированной среде версия CRT выбирается в свойствах проекта: на закладке C/C++ в категории Code Generation.

Плохая новость заключается в том, что MSVCRT.DLL существует не на всех версиях Windows. Она начала поставляться в составе ОС, начиная с Windows 95 OSR2. Приложение, запущенное в системе без этой библиотеки, выполняться не будет. Правда, таких систем становится все меньше и меньше.

Уменьшение выравнивания файловых секций

Возможно, владельцы Visual C++ 5.0 заметили, что у них в результате получаются EXE-файлы куда меньшего размера, чем сказано здесь. Дело в том, что компоновщик версии 5.0 использовал выравнивание секций исполняемого файла на величину 512 байт. Начиная же с версии 6.0, при сборке приложения используется другая величина выравнивания - 4К. Это позволяет быстрее загружать такой файл в Windows 98 и более новых версиях ОС.

Вернуть прежнюю величину выравнивания можно, задав недокументированную опцию компоновщика /opt:nowin98:

cl /MD test.cpp user32.lib /link /opt:nowin98

Размер EXE в результате составляет менее 3-х килобайт! Но не забудьте, что такой файл будет медленнее загружаться в память, и что он по-прежнему требует наличия MSVCRT.DLL.

Радикальные меры: отказываемся от CRT Startup

Если ампутация кажется вам разумной хирургической операцией, то стартовый код CRT можно выбросить из программы совсем.

Что это означает? Отказавшись от некоторых привычных удобств, которые предоставляет CRT, можно писать на C/C++, не используя возможностей, которые требуют поддержки со стороны CRT.

В мире Windows API такое решение не пугает многих. Взгляните, например, на NullSoft Installer.

В самом деле, для файловых операций можно использовать функции Win API, вместо динамической памяти C++ использовать кучу (хип) Windows, для форматирования можно использовать wsprintf вместо sprintf, для сравнения строк - lstrcmp вместо strcmp и т.д.

При этом важно понимать, что CRT - это обычная библиотека, функции которой вполне можно вызывать из такой программы (как и из программы на ассемблере). Главное - это отказаться от функций, которые влекут за собой включение раздутого кода инициализации (или, в крайнем случае, включить его необходимую часть самостоятельно).

Мэтт Питрек, давний ведущий колонки "Under The Hood" в Microsoft Systems Journal (ныне - MSDN Magazine), посвятил этому вопросу цикл статей в MSJ под общей тематикой "Code Liposuction" ("обезжиривание кода"). Интересующиеся могут найти их в архиве Periodicals MSDN.

Более свежая информация содержится в его статье "Reduce EXE and DLL Size with LIBCTINY.LIB" в январском выпуске MSDN Magazine за 2001 год. Предлагаемая автором версия "крохотной" библиотеки исполнения выполняет минимальную инициализацию (например, вызывает конструкторы глобальных объектов) и даже предоставляет собственные версии таких функций, как printf и malloc. При этом размер выполняемого модуля оказывается зачастую меньше 3 Кб.

Но не будем забираться так далеко - ведь в нашем коде нет никаких конструкторов, правда?

В данном случае можно просто указать, что функция main будет точкой входа в программу (вместо функции инициализации):

cl test.cpp user32.lib /link /entry:main /opt:nowin98 /subsystem:console

В результате также получим исполняемый файл размером менее 3 Кб (я вновь использовал опцию /opt:nowin98). Разница теперь лишь в том, что он не требует внешней CRT-библиотеки (библиотека user32.lib необходима для функции MessageBox, но она является частью ядра Windows).

Версия ATL: макрос _ATL_MIN_CRT

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

В составе библиотеки ATL версии 3 и более ранних имеется файл atlimpl.cpp. Он, как правило, включается в один из исходных файлов проекта (чаще всего в stdafx.cpp) с помощью директивы #include. В atlimpl.cpp находится "облегченная" реализация стартового кода CRT: в нее входят только вариант функции xxxCRTStartup, упомянутой ранее, и "обертки" для работы с динамической памятью - функции malloc, calloc, realloc, free и операторы new/delete. Они непосредственно вызывают функции Windows для работы с кучей - HeapAlloc и HeapFree. Как ни странно, этого достаточно, чтобы заставить заработать без CRT startup множество программ.

Собственно, сама эта реализация доступна, только если определен символ препроцессора _ATL_MIN_CRT. Таким образом, есть возможность легко управлять включением или исключением стартового кода CRT.

ПРИМЕЧАНИЕ

Важный момент при использовании макроса _ATL_MIN_CRT: по-прежнему нельзя включать объявления глобальных переменных, классы которых имеют конструкторы или деструкторы, так как код, их вызывающий, содержится только в CRT.

Эта проблема решена в библиотеке ATL 7.0 (не удивляйтесь, как и многие другие приложения Microsoft, ATL перескочила с версии 3 на версию 7), поставляемой с компилятором MS VC++ 7.0. Тем же, кто пользуется прежними версиями компилятора, могу посоветовать воспользоваться отличной библиотекой Andrew Nosenko's ATL/AUX Library, в которой содержится код вызова конструкторов/деструкторов. Для этого необходимо включать в проект вместо atlimpl.cpp файл AuxCrt.cpp из комплекта библиотеки.
Кто виноват?

Теперь ясно, что причиной появления ошибки "unresolved external symbol _main" стало включение стартового кода CRT. То есть, была явно или неявно использована какая-либо функция, которая содержит ссылку на структуру данных, находящуюся в модуле с кодом инициализации. При включении компоновщиком в программу этого модуля возникает следующая внешняя ссылка: в теле mainCRTStartup есть вызов main. Вот и все, мы получили наше "любимое" сообщение об ошибке.

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

  • Включается опция компоновщика /verbose, при которой он выдает значительно большее количество диагностической информации.
  • Включается опция компоновщика /nodefaultlib (или /nod), которая подавляет при сборке поиск библиотек, кроме указанных явно. При этом в списке неразрешенных внешних ссылок будут как "безобидные" функции CRT (которые можно будет включить явно), так и "тянущие" за собой стартовый код CRT.
  • Локализовав модуль или функцию проекта, в которой появилась нежелательная внешняя ссылка на CRT, можно включить генерацию ассемблерного листинга (опция компилятора /FA) и простым поиском обнаружить, где происходит реальное включение.
Использование Standard Template Library

А как же насчет Standard Template Library (STL)? Насколько она завязана на CRT, можно ли использовать её в сверхмалых проектах?

Реализация STL от Dinkumware, поставляемая вместе с VC 5.0 и 6.0, доступна в исходных файлах, так что проблем с компоновкой не возникает. В крайнем случае, всегда можно исправить исходники или сделать какую-нибудь заглушку на #define'ах (перебивающую имена конструкций, тянущих за собой CRT). Другая проблема - в том, что STL повсеместно использует операторы динамического выделения памяти. Как уже говорилось, это вызывает необходимость собственной реализации операторов new/delete. Это можно сделать, например, так (идея позаимствована из atlimpl.cpp):

// stub.cpp - the "mini-CRT" implementation file

void* __cdecl malloc(size_t n)
{
    void* pv = HeapAlloc(GetProcessHeap(), 0, n);
    return pv;
}
void* __cdecl calloc(size_t n, size_t s)
{
    return malloc(n*s);
}
void* __cdecl realloc(void* p, size_t n)
{
    if (p == NULL) return malloc(n);
    return HeapReAlloc(GetProcessHeap(), 0, p, n);
}
void __cdecl free(void* p)
{
    if (p == NULL) return;
    HeapFree(GetProcessHeap(), 0, p);
}
void* __cdecl operator new(size_t n)
{
    return malloc(n);
}
void __cdecl operator delete(void* p)
{
    free(p);
}

Вот пример программы, которая будет спокойно собрана с помощью такого подхода без стартового кода CRT:

#include <windows.h>
#include "stub.cpp"
#include <map>

typedef std::map<int, int> IntMap;

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
    LPSTR lpCmdLine, int nShowCmd   )
{
    IntMap m;
    for (int j=0; j<100; j++) m[j*j]=j;
    IntMap::iterator i=m.find(49);
    MessageBox(0, (i==m.end())?"49 was not found":"49 was found",
        "std::map test", MB_OK);
    return 0;
}

Для сборки этого примера необходимо использовать следующую командную строку:

cl test.cpp user32.lib kernel32.lib /link /nod /opt:nowin98
                             /subsystem:windows /entry:WinMain

Библиотека импорта kernel32.lib необходима для функций работы с Win32-кучей.

Что касается других реализаций STL, предоставлю слово Павлу Блудову:

"Страшная тайна STL от SGI и HP в том, что им совершенно не нужна CRT.

С двумя оговорками:

  1. Не используется C++ Exception Handling
  2. (Вытекает из первой) определен макрос __THROW_BAD_ALLOC, например, так:
#ifndef _CPPUNWIND
#define __THROW_BAD_ALLOC \
::MessageBox(NULL, _T("STL: Out of memory."), NULL, MB_OK | MB_ICONSTOP); \
::ExitProcess(-5);
#endif _CPPUNWIND
#include <stl_config.h>

если посмотреть на __THROW_BAD_ALLOC, то он являет собой

#define __THROW_BAD_ALLOC fprintf(stderr, "out of memory\n"); exit(1)

именно эта строчка, и никакая другая, нуждается в CRT. Ну, если быть совсем точным, std::string'у может понадобиться CRT. Тут уж ничего не попишешь. Используйте WTL::CString.

Павел."

Слова о std::string в полной мере справедливы и для реализации STL от Dinkumware. Если вы ищете реализацию полноценного строкового класса, не использующего стартовый код CRT, советую взглянуть на CascString в составе библиотеки ascLib.

Директива #import и ее ограничения в облегченных проектах

Частой причиной появления зависимости от CRT является необдуманное применение директивы #import - расширения Visual C++ для удобства работы с COM-объектами, предоставляющими библиотеки типов. Подробнее о ней можно прочитать в MSDN, а на русском языке - в статье Игоря Ткачева "Использование директивы #import в Visual C++".

При ее использовании компилятор генерирует описания интерфейсов и, если не указано обратное, создает набор оберточных классов (wrappers) для упрощения работы с указателями на эти интерфейсы. Кроме того, детали реализации COM-объектов скрываются за высокоуровневыми средствами. В число таких деталей входят преобразование [out,retval]-параметров в возвращаемые значения функций, упрощение работы с BSTR-строками, управление сроками жизни объектов, доступ к свойствам и преобразование COM-HRESULT в исключения С++. Но поддержка всех этих приятных "мелочей" реализована с использованием CRT и требует включения стартового кода CRT.

Директива #import, несомненно, полезна для C++-программиста - ведь иначе, не имея описания интерфейсов, пришлось бы извлекать необходимую информацию вручную с помощью утилит типа OleView. Эту директиву можно применять и в проектах, не использующих CRT, но с рядом ограничений. В частности, необходимо подавить создание оберточных классов и трансляцию типов COM в классы-обертки _com_ptr, _com_error, _variant_t и _bstr_t. Вот пример выверенного использования #import, которое не "потянет" за собой половину кода CRT:

#import "file.dll" no_namespace, \
  named_guids, no_implementation, \
  raw_interfaces_only, raw_dispinterfaces, \
  raw_native_types

Иногда при использовании #import можно обойтись "малой кровью". Это возможно, например, если в интерфейсах импортируемой библиотеки типов не используются BSTR- и VARIANT-параметры (вообще-то, достаточно редкий случай). Тогда можно воспользоваться всеми удобствами, предоставляемыми #import, но подавить генерацию исключений C++ при возврате ошибок. Для этого потребуется реализовать функцию

void __stdcall _com_issue_error(HRESULT hr);

Такая возможность определяется в каждом конкретном случае экспериментально. Все же, если вы не используете исключения, лучше отказаться от расширенной помощи директивы #import и обрабатывать HRESULT вручную.

ПРИМЕЧАНИЕ

В составе уже упомянутой библиотеки ATL/AUX есть средство автоматической генерации классов из библиотек типов, которое более пригодно для сверхмалых проектов, чем директива #import.
Использование вычислений с плавающей точкой

В статьях, посвященных использованию макроса _ATL_MIN_CRT, часто говорится, что в минимальных ATL-проектах нельзя использовать вычисления с плавающей точкой. К счастью, это не так. Уже давно миновали времена, когда программа на C++ не могла стартовать без кода эмуляции сопроцессора. Но трудности все-таки остались, и их придется обходить, поэтому:

постарайтесь использовать fixed-арифметику вместо floating point-вычислений

Это означает, что, например, для расчета суммы с точностью до копеек можно просто выразить сумму в копейках. Другой вариант этой методики - выделить несколько (например, 16) двоичных разрядов числа на целую часть, а оставшуюся часть считать дробной. Тогда целая часть числа получается простым сдвигом вправо или обращением по следующему адресу, что очень быстро. Такие расчеты здорово помогут там, где нужны только целые числа, но рассчитанные с хорошей точностью - например, в машинной графике.

Если floating-point вычисления необходимы, попробуйте обмануть компилятор с помощью _fltused

Встретив в программе объявление float-переменной (или double), компилятор автоматически вставляет в генерируемый код внешнюю ссылку на переменную _fltused, находящуюся в одном из файлов CRT. Это делается для того, чтобы прилинковать к программе код обработчика ошибок вычислений с плавающей точкой.

В случае, когда нельзя обойтись без плавающей арифметики, но код заведомо не вызовет ошибок, можно попытаться отключить стартовый код CRT для плавающих вычислений с помощью примерно такого объявления:

extern "C" int _fltused = 0;

Переменная оказывается определенной внутри модуля, и линкеру незачем искать ее где-нибудь еще.

Но фокус не сработает, если произойдет вызов функции CRT. Это может случиться неявно, например, при преобразовании между целочисленными и плавающими типами:

double t;
int a;
a = t; // Получили внешнюю ссылку на функцию _ftol

Правда, _ftol - это как раз пример функции CRT, которая может быть безболезненно использована в минимальной программе. Просто укажите в списке библиотек LIBC.LIB и позаботьтесь о том, чтобы обеспечить компоновщик своей версией стартового кода (при использовании _ATL_MIN_CRT ничего дополнительно делать не нужно).

Если же вызываемая неявно функция требует инициализации, есть два пути: отказаться от борьбы или реализовать ее иным способом, о чем сейчас и пойдет речь.

Несколько рекомендаций

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

Забудьте об этом, если используете MFC

Библиотека MFC требует наличия кода инициализации, и тут уж ничего не поделаешь. Если очень хочется использовать библиотеку оконных классов в сверхмалых проектах, посмотрите в сторону ATL/WTL и их расширений (например, Attila).

Используйте SEH вместо C++ Exceptions

Обработка исключений в стиле C++ неизбежно потребует стартового кода CRT. Если исключения использовать необходимо, попробуйте воспользоваться структурными исключениями Win32 с помощью ключевых слов __try, __except, __finally и т.д. Для их использования нужно подключить библиотеку импорта kernel32.lib.

Попробуйте позаимствовать необходимую функцию из исходных файлов CRT

Visual C++ поставляется с большим набором исходных файлов, в число которых входит и реализация CRT. Их изучение, кстати, приносит и еще одну выгоду - это поможет разобраться, как именно устроена поддержка стандартной библиотеки. В общем, "Use the source, Luke"!

Используйте директиву #pragma intrinsic

Некоторые функции, требующие инициализации CRT, могут быть попросту вставлены компилятором в точку вызова. К ним относятся cos, strlen и многие другие. Изучите документацию на #pragma intrinsic и опцию компилятора /Oi.

Для преобразования типов воспользуйтесь Automation API

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

Можно использовать как функции высокого уровня VariantChangeType/VariantChangeTypeEx, так и вспомогательные функции преобразования вида VarXXXFromYYY. В приведенном выше блоке кода, например, поможет функция VarI4FromR8:

double t;
long a;
VarI4FromR8(t, &a); // Никаких проблем с внешними ссылками

Использование Automation API позволит не только решить проблему преобразования, описанную выше, но и учесть при этом региональные настройки - например, получить локализованную строку даты. Кроме того, функция VarBstrCmp поможет при сравнении строк Unicode (но будьте осторожны с ней - в старых версиях Windows она отличается от новых реализаций, также нужно иметь установленный Service Pack 4 или выше для VC, иначе заголовочный файл будет содержать некорректное описание этой функции).

Для использования этих функций необходимо подключить библиотеку импорта oleaut32.lib.

До следующей встречи!

Алекс Jenter jenter@rsdn.ru
Duisburg, 2001. Публикуемые в рассылке материалы принадлежат сайту RSDN.


ПРОГРАММИРОВАНИЕ НА V I S U A L C + +
РАССЫЛКА САЙТА
RSDN.RU
Выпуск No. 62 от 3 февраля 2002 г.

РАССЫЛКА ЯВЛЯЕТСЯ ЧАСТЬЮ ПРОЕКТА RSDN , НА САЙТЕ КОТОРОГО ВСЕГДА МОЖНО НАЙТИ ВСЮ НЕОБХОДИМУЮ РАЗРАБОТЧИКУ ИНФОРМАЦИЮ, СТАТЬИ, ФОРУМЫ, РЕСУРСЫ, ПОЛНЫЙ АРХИВ ПРЕДЫДУЩИХ ВЫПУСКОВ РАССЫЛКИ И МНОГОЕ ДРУГОЕ.

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

CТАТЬЯ

Написание Plugin'ов для Internet Explorer
Автор: Борис Гулай aka BoresExpress
Источник: журнал "Программист" ?1 за 2002

Исходные тексты примера - 11 KB

Всем памятны обвинения в адрес Microsoft в том, что включение браузера Microsoft Internet Explorer в состав операционной системы Windows недопустимо. Ответом корпорации было то, что браузер является неотъемлемой частью системы. Теперь мы можем сказать даже больше - Internet Explorer как единое приложение не существует. Это набор компонентов, которые собираются в единое целое только при запуске приложения. Сейчас мы попробуем включить в этот стройный ряд компонентов свой, чтобы он тоже стал неотъемлемой частью, ну если не операционной системы, то конкретной копии браузера точно.

Архитектура Internet Explorer

Что мы будем делать?

Что же представляет собой плагин для Internet Explorer? Это обычный внутрипроцессный (In Process) COM-сервер (т.е. DLL-файл), который содержит объект, реализующий как минимум 2 интерфейса: IOleCommandTarget и IObjectWithSite. Кроме того, наш dll-файл должен экспортировать не менее 2 функций: DllGetClassObject и DllCanUnloadNow. Думаю, их назначение всем известно.

Наш плагин будет очень простым. Он будет сохранять все ссылки страницы, которые указывают на файлы с заданными в .ini-файле расширениями в результирующий файл. Такой плагин может быть полезен, например, при создании списков закачиваемых файлов для download-менеджеров. Искать и сохранять ссылки он будет при нажатии на кнопку, которую мы добавим на панель инструментов браузера, или при выборе соответствующего пункта в меню 'Сервис'. А кнопку и пункт меню мы будем делать доступными (enabled) только в том случае, если в браузере открыт файл с расширением .htm или .html (это мы сделаем просто для демонстрации такой возможности).

Как это работает?

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

Прежде всего, браузер загружает нашу библиотеку, это происходит вместе с загрузкой самого IE. Затем, после первого нажатия на кнопку (!), он вызывает экспортируемую функцию DllGetClassObject и запрашивает у неё указатель на интерфейс IClassFactory.Далее, из полученного интерфейса он вызвает метод CreateInstace и запрашивает у него интерфейс IUnknown. Это должен быть IUnknown компонента, который реализует и IOleCommandTarget и IObjectWithSite.

Два вышеназванных интерфейса должны быть реализованы именно в одном компоненте. Internet Explorer будет запрашивать один через QueryInterface другого. Поэтому реализовать их отдельно нет никакой возможности.

Такое поведение контейнера выглядит логичным, если принять во внимание то, зачем компоненту интерфейс IObjectWithSite. Через его метод SetSite браузер передаёт указатель на интерфейс, через который можно добраться до IWebBrowser - основного интерфейса WebBrowser Control. Это может потребоваться компоненту, при обработке нажатия на кнопку или выбора пункта меню, если он захочет узнать, в каком контексте произошло это событие. Поэтому совершенно логично, что IObjectWithSite должен реализовывать тот же компонент, который обрабатывает нажатие на кнопку.

После того, как произошло первое нажатие на кнопку, Internet Explorer вызывает метод SetSite интерфейса IObjectWithSite и передаёт в него IUnknown объекта, реализующего интерфейс IShellBrowser. Хочу обратить Ваше внимание, что вызов вышеназванного метода происходит только один раз.

Затем, в ответ на нажатие кнопки, вызывается метод IOleCommandTarget::Exec, в котором и происходит обработка события.

После вызова IObjectWithSite::SetSite IE периодически вызывает метод IOleCommandTarget::QueryStatus, где плагин может, при необходимости, изменить статус своей кнопки и пункта меню (enabled/disabled).

При завершении своей работы браузер вызывает IObjectWithSite::SetSite со значением NULL в качестве единственного аргумента, что говорит плагину о необходимости освободить (Release) сохранённый после первого вызова SetSite интерфейс браузера (если он его сохранял, конечно). Затем IE освобождает все интерфейсы плагина и при положительном ответе функции DllCanUnloadNow выгружает плагин.

Так выглядят, в общих чертах, то, что нам придётся запрограммировать.

Как это написать?

После знакомства с механизмом интеграции плагинов в Internet Explorer, мы можем приступать к написанию кода. Я предполагаю, что читатель знаком с основами COM, поэтому не буду описывать создание COM-сервера и добавление в него компонентов. А сразу перейду к самому интересному - реализации методов интерфейсов, которые необходимы плагину для полноценного функционирования.

Следует сразу (пока Вы ещё не успели начать работу) сказать, что метод IObjectWithSite::GetSite в реализации не нуждается (хотя в примере он и реализован), т.к. браузер его никогда не вызывает (он ведь всегда знает, какая страница в нём открыта).

Начнём мы с самого простого, а именно с метода IObjectWithSite::SetSite. Для начала добавим в объявление объекта переменную типа IWebBrowser2Ptr (я предпочитаю использовать то, что в MSDN называется compiler COM support classes; это значительно ускоряет работу). Через эту переменную мы всегда будем иметь доступ ко всем предоставляемым браузером интерфейсам.

Код этого метода выгладит следующим образом:

STDMETHODIMP IMyIEExtention::SetSite(IUnknown *pUnkSite)
{
    if (!pUnkSite)
    {
        if (m_pWebBrowser2.GetInterfacePtr())
            m_pWebBrowser2.Release();
        return S_OK;
    }

    IServiceProviderPtr pServProv(pUnkSite);
    return pServProv->QueryService(SID_SWebBrowserApp, IID_IWebBrowser2,
            (void**)&m_pWebBrowser2);
}

В начале я проверяю, не хочет ли IE сказать мне этим вызовом, что происходит завершение его работы и я должен освободить его интерфейсы. Дальше - интересней. Я запрашиваю интерфейс IWebBrowser2, но не как обычно, через вызов QueryInterface, а посредством вызова метода QueryService, предварительно полученного интерфейса IServiceProvider.

Очередное отступление, на этот раз про необходимость таких странных манипуляций для решения, казалось бы, стандартной задачи. Интерфейс IServiceProvider предназначен для использования в следующих ситуациях.

Предположим, существует некое приложение-контейнер, которое использует несколько COM-серверов. У каждого из них, естественно, есть доступ к интерфейсам контейнера (посредством IObjectWithSite::SetSite, например). Но вот кому-то из COM-серверов потребовалось получить доступ к интерфейсам другого COM-сервера, также содержащегося в контейнере.

Как же ему решить эту задачу? Ведь стандартными средствами он до другого сервера никак не доберётся, поскольку контейнер, в соответствии с идеологией COM, не предоставляет доступ к интерфейсам содержащихся в нём объектов непосредственно через вызовы QueryInterface своих интерфейсов.

Для решения таких задач как раз и предназначен интерфейс IServiceProvider. Его единственный метод - QueryService - отличается от QueryInterface одним параметром - идентификатором сервиса. Фактически - это идентификатор одного из COM-компонентов, используемых приложением-контейнером. И когда COM-сервер хочет получить интерфейс другого сервера, используемого тем же клиентом, он просто вызывает вышеназванный метод с соответствующим идентификатором сервиса.

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

Возвращаясь к нашей задаче, легко заметить, что здесь аналогичная ситуация. Internet Explorer представляет собой зоопарк компонентов, где наш COM-сервер (т.е. плагин) - один из питомцев. Поэтому нам и приходится использовать вышеописанную технику для получения доступа к интерфейсам другого компонента (которым, в нашем примере, является WebBrowser Control).

Следующим в очереди на реализацию у нас стоит метод QueryStatus интерфейса IOleCommandTarget. Его текст выглядит следующим образом:

STDMETHODIMP IMyIEExtention::QueryStatus(const GUID *pCmdGroup,
                          ULONG cCmds, OLECMD prgCmds[], OLECMDTEXT *pCmdText)
{
    if (!prgCmds)
        return E_POINTER;
    ASSERT(cCmds == 1);
    if (!cCmds)
        return E_UNEXPECTED;

    BSTR url;
    HRESULT hRes=S_OK;
    hRes=m_pWebBrowser2->get_LocationURL(&url);
    CHECK_COM_RESULT(hRes)
    bstr_t pszUrl(url, false);

    LPCTSTR pExt=(LPCTSTR)pszUrl+pszUrl.length()-5;
    if (!_tcsicmp(pExt, _T(".html")) || !_tcsicmp(pExt+1, _T(".htm")))
        prgCmds[0].cmdf=OLECMDF_ENABLED;
    else
        prgCmds[0].cmdf=OLECMDF_SUPPORTED;

    return S_OK;
}

В начале необходимо удостовериться в корректности переданных данных. Затем мы просто запрашиваем текущий URL и, если его последние символы .htm или .html, делаем кнопку и пункт меню доступными, или недоступными в противном случае. Следует заметить, что в этот метод всегда должен передаваться только один элемент в массиве prgCmds, т.к. мы отвечаем только за одну кнопку и пункт меню.

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

Как это подключить?

Теперь остался последний штрих - регистрация нашего компонента в реестре. В первую очередь, необходимо корректно зарегистрировать наш плагин как COM-сервер. Я не буду описывать эту процедуру здесь, поскольку это лежит за рамками моей статьи, да и информации на эту тему немало. Остановимся подробнее на регистрации нашей DLL в качестве плагина для Internet Explorer.

Для этого необходимо создать следующий ключ в реестре:

<key root>\Software\Microsoft\Internet Explorer\Extensions \<ваш GUID>

В качестве <key root> может выступать либо HKEY_CURRENT_USER (в этом случае плагин будет доступен только текущему пользователю), либо HKEY_LOCAL_MACHINE (плагин будет доступен всем пользователям).

Теперь в нём необходимо создать следующие параметры:

ButtonText | Текст всплывающей подсказки для кнопки. Значение может быть как текстом, так и строкой следующего формата @dll_path,-ID, где dll_path путь к DLL плагина, ID - идентификатор строки в string table.
CLSID | Всегда {1FBA04EE-3024-11d2-8F1F-0000F87ABD16}
Default Visible | Будет ли кнопка, сразу после регистрации плагина, находиться на панели ('yes') или пользователь должен будет добавить её на панель самостоятельно ('no' или если параметр отсутствует).
ClsidExtension | GUID плагина, как COM-сервера (из раздела HKCR\CLSID).
HotIcon | Путь к иконке, соответствующей активному состоянию кнопки (когда на неё наведена мышь). Если путь указывает на .dll или .exe файл, то после него, через запятую, указывается идентификатор ресурса.
Icon | Путь к иконке, соответствующей обычному состоянию кнопки.
MenuText | Текст пункта в меню сервис.
MenuStatusBar | Текст подсказки, появляющейся в строке состояния, когда пункт меню активен (формат аналогичен параметру ButtonText).

Файл, на который указывает параметр HotIcon, должен содержать следующие цветные значки:

  • 16х16 16 цветов
  • 20х20 16 цветов (не обязательно)
  • 20х20 256 цветов

Второй файл (соответствующий параметру Icon) должен содержать значки в оттенках серого. Параметры этих значков следующие:

16х16 16 оттенков серого

  • 20х20 16 оттенков серого (не обязательно)
  • 20х20 256 оттенков серого

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

Выполнение операций по добавлению информации в реестр логично возложить на функцию DllRegisterServer, экспортируемую нашей DLL. Именно так сделано в примере к этой статье. Также в демонстрационном приложении реализована функция DllUnregidterServer, удаляющая всю информацию о плагине из реестра.

Что в итоге?

Теперь, если вы следовали приведённым выше действиям, на панели инструментов Internet Explorer должна появиться кнопка, а в меню 'Сервис' строка меню, запускающая наш плагин.

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

ЭКЗАМЕН

How do you create an edit control that echoes '#' for any character the user types?

  1. Super-class the edit control, override the WM_CHAR message, and also override the WM_PAINT message to echo the '#' char.

  2. Send the edit control a WM_PASSWORD message and then send a WM_SETPASSWORDHAR message to specify the '#' char.

  3. Send the edit control an EM_PASSWORD message and then send an EM_SETPASSWORDHAR message to specify the '#' char.

  4. Use the ES_PASSWORD style and then send an EM_SETPASSWORDCHAR message to specify the '#' char.

  5. Sub-class the edit control, override the WM_CHAR message, and also override the WM_PAINT message to echo the '#' char.

Вариант 1 неверен, т.к. действие "super-class the edit control" не имеет смысла. Варианты 2 и 3 также неверны, потому что сообщений WM_PASSWORD, WM_SETPASSWORDCHAR и EM_PASSWORD не существует. Вариант 5 неверен, т.к. существует стандартный и более простой способ. Правильный ответ - вариант 4, существует и стиль ES_PASSWORD, и сообщение EM_SETPASSWORDCHAR, которые предназначены как раз для решения этой проблемы.

Это все на сегодня. Пока!

Алекс Jenter jenter@rsdn.ru
Duisburg, 2001. Публикуемые в рассылке материалы принадлежат сайту RSDN.


ПРОГРАММИРОВАНИЕ НА V I S U A L C + +
РАССЫЛКА САЙТА
RSDN.RU
Выпуск No. 63 от 10 февраля 2002 г.

РАССЫЛКА ЯВЛЯЕТСЯ ЧАСТЬЮ ПРОЕКТА RSDN , НА САЙТЕ КОТОРОГО ВСЕГДА МОЖНО НАЙТИ ВСЮ НЕОБХОДИМУЮ РАЗРАБОТЧИКУ ИНФОРМАЦИЮ, СТАТЬИ, ФОРУМЫ, РЕСУРСЫ, ПОЛНЫЙ АРХИВ ПРЕДЫДУЩИХ ВЫПУСКОВ РАССЫЛКИ И МНОГОЕ ДРУГОЕ.

Здравствуйте, дорогие друзья!

CТАТЬЯ

Растровые изображения с прозрачными областями
Автор: Ron Gery
Перевод: Виталий Брусенцев
Демонстрационная программа к статье (12 кБ)
Предисловие переводчика

Да, эта статья написана в 1992 году, и самая свежая информация в ней относится к Windows 3.1. Но описываемые здесь алгоритмы и методы растровой графики до сих пор (за одним-двумя исключениями) работают в Windows. Также приятно, что приведенные алгоритмы описаны доходчиво и в деталях. В-общем, если Вы хотите разобраться в том, как работает прозрачность в Windows GDI - читайте эту статью!

Введение

Установив с помощью вызова функции SetBkMode() режим отображения фона как TRANSPARENT, можно выводить текст с прозрачным фоном, пунктирные линии с прозрачными разрывами и кисти с прозрачными областями. К сожалению, среда Windows не предоставляет таких же простых средств для вывода прозрачных растров. (Ну хорошо, представляет, но поддерживается этот метод далеко не везде - подробнее об этом ниже, в разделе "Простая растровая прозрачность".) К счастью, можно сымитировать этот эффект, используя маскирующий растр и несколько раз вызвав функцию BitBlt с правильными параметрами растровых операций.

Что из себя представляет растр с прозрачностью? Это растровая картинка, сквозь которую видна часть фонового изображения. Простой пример этого - иконка Control Panel. [Здесь речь идет о системе Windows 3.x - прим. перев. ] Эта иконка, вообще-то - прямоугольник, но когда Control Panel минимизируется, сквозь некоторые ее части просматривается рабочий стол. Говоря упрощенно, иконка - прямоугольный растр, некоторые пикселы которого помечены прозрачными. При отображении на экран они не изменяют область назначения. Еще более интересно применение прозрачности растровых изображений в задачах движения, непрямоугольности картинок и т.д. Изложенные методы имитации помогут решить эти и другие проблемы, связанные с прозрачностью.

Обозначения

В этой статье для описания пикселов исходного растрового изображения используются термины "прозрачный" (transparent) и "непрозрачный" (opaque). Прозрачными будем называть пикселы, которые не влияют на конечное изображение. Непрозрачные пикселы рисуются поверх точек назначения, заменяя их собой.

Предполагается, что черный цвет кодируется значениями "0" во всех двоичных разрядах, а белый цвет - значениями "1" соответственно. Это выполняется на всех известных графических драйверах Windows, включая основанные на палитре.

Базовая описываемая операция - перенос (blting) битов изображения из источника в место назначения. Дополнительные операции переноса используют монохромную маску. Источник и приемник представляют собой хэндлы графического контекста (HDC). Они обозначаются hdcSrc и hdcDest соответственно и могут представлять как растр (в памяти), так и непосредственно графическое устройство. Маска, обозначаемая hdcMask, должна представлять собой монохромное растровое изображение, выбранное в совместимом графическом контексте.

Основные понятия

До того, как перейти к собственно рисованию, вспомним основные понятия, используемые в растровой графике.

Растровая операция

Последний параметр функции BitBlt указывает код растровой операции (ROP). Он определяет, какая комбинация битов источника, приемника и шаблона (текущей выбранной кисти) создаст конечную картинку. Так как растр - всего лишь набор битов, ROP можно назвать булевским уравнением над битами. В зависимости от вида графического устройства, биты растра имеют разное значение:

  • Для монохромных устройств каждый бит представляет один пиксел: черный - значением 0, белый - значением 1.
  • Для цветных устройств каждый пиксел описывается набором битов - либо индексом в таблице цветов (палитре), либо непосредственным значением цвета.

Вне зависимости от конкретного назначения битов, ROP просто выполняет действия над ними.

Весь фокус заключается, конечно же, в том, чтобы получить осмысленную комбинацию битов. В приложении A к Руководству программиста по Windows 3.1 SDK приведен список из 256 возможных тернарных ROP. Они предоставляют множество способов комбинировать растровые данные, и зачастую один и тот же эффект можно получить разными путями. В этой статье мы будем иметь дело лишь с четырьмя ROP.

ПРИМЕЧАНИЕ
Тернарная операция - это операция над тремя операндами. Применительно к растрам это означает взаимодействие битов источника, назначения и выбранной в контексте устройства кисти (Brush или Pattern). Список упоминаемых здесь кодов ROP вы можете найти в MSDN в разделе Platform SDK/Graphics And Multimedia Services/ Windows GDI/Painting And Drawing/Painting And Drawing Reference/Raster Operation Codes. У наиболее применимых кодов ROP существуют символические имена, определенные в заголовочном файле windows.h
- прим. перев.
Название | Логическая операция | Как используется при имитации прозрачности
SRCCOPY | src | Копирует источник (src) непосредственно на место назначения (dst).
SRCAND | src AND dest | Заполняет черным цветом те области назначения, которым в источнике соответствуют области черного цвета. Не затрагивает те области назначения, которым в источнике соответствуют области белого цвета.
SRCINVERT | src XOR dest | Производит операцию логического умножения (XOR) над битами источника и приемника. Результат помещает в приемник. При повторном применении восстанавливает предыдущее состояние. При некоторых обстоятельствах можно использовать вместо SRCPAINT.
SRCPAINT | src OR dest | Отрисовывает не-черные области источника на приемнике. Черные области источника не влияют на приемник.

Некоторые принтеры не поддерживают определенные коды растровых операций - в особенности ROP, которые затрагивают область назначения. По этой причине описываемые здесь методы касаются дисплейных устройств и не обязательно будут работать на принтерных (таких, как PostScriptR).

Маски прозрачности

В этой статье слово "маска" означает не ту штуку, которую Бэтмен носит на лице, а растр, ограничивающий видимую порцию другого растра. Маска содержит непрозрачную составляющую (черную), "сквозь которую" виден исходный растр, и прозрачную (белую) область, в которой пикселы приемника останутся нетронутыми. Так как маска состоит лишь из двух цветов, ее удобно представлять в виде монохромного растра [т.е., растра с форматом 1 бит на пиксел - прим. перев.]. Но ничто не помешает хранить такую маску в многоцветном растре (но содержащем лишь черные и белые пикселы). Как обсуждается ниже, в разделах "Метод истинной маски" и "Метод черного источника", перенос маски является частью многопроходного процесса рисования: он подготавливает приемник к окончательной отрисовке исходного растра с прозрачностью. Приводимое в качестве примера приложение TRANSBLT использует монохромную маску с пикселами, равными 1 для прозрачных и 0 для непрозрачных областей. При желании приложение может обращать эти два значения и компенсировать это в процессе преобразования из монохромного формата в цветной, как описано ниже в этом разделе.

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

Преобразование из монохромного формата в цветной

Имитация прозрачности может также включить имеющийся в Windows механизм преобразования растров из черно-белого формата в цветной (и наоборот). Для отображения между форматами используется принятые в Windows обозначения: цвет текста (text color, foreground color) и цвет фона (background color). Во время переноса бит на цветной приемник монохромный источник (и, если необходимо, кисть) "на лету" преобразуется в цветной формат - до того, как выполнится ROP над битами. Пикселы со значением 0 (черные) преобразуются в цвет текста назначения, а, соответственно, белые (со значением 1) - в цвет фона. И наоборот, когда формат назначения - монохромный, Windows преобразует цветной источник в этот формат. В этом случае все пикселы источника, имеющие цвет, совпадающий с цветом фона, становятся единицами в битовом представлении, а пикселы с цветом текста - нулями. Так как во всех приводимых ниже примерах используется монохромная маска, для приложения жизненно важно правильно установить цвета текста и фона (с помощью вызовов SetTextColor и SetBkColor) перед выполнением операций переноса.

Производительность и мерцание

Интенсивные растровые операции ведут к падению производительности из-за вовлечения большого количества бит в обработку. Кроме того, при выводе непосредственно на экран возникает мерцание - чем больше размер затрагиваемой области, тем заметнее. Хотя не существует волшебного способа ускорить обработку, мерцание можно устранить - используя "теневые" растры. Для этого в растр, находящийся в памяти, копируется область экрана, в которую будет происходить вывод. Затем над этим растром (вместо непосредственно экрана) производятся необходимые операции, например, для получения эффекта прозрачности. И наконец, сформированный "теневой" растр выводится на экран. Мерцание устраняется, так как содержимое экрана изменилось всего один раз. Очевидно, что два дополнительных переноса бит вызовут падение скорости (хотя на некоторых устройствах перенос в/из памяти будет быстрее, чем с участием экрана), но исчезновение мерцания может создать ощущение того, что работа приложения ускорилась (по-разному, в зависимости от вида обработки и реального размера растров). Вывод также становится намного симпатичнее без мерцания. Необходимость применения "теневых" растров определяется исходя из назначения приложения.

Метод истинной маски

Для работы данного метода не требуется никаких изменений в исходном растре, что может быть полезно. Маскированный перенос использует трехпроходный процесс и маску, содержащую прозрачные (со значением 1) и непрозрачные (со значением 0) пикселы. Вот пример псевдокода:

// Подготовить приемник для монохромного переноса (необходимо только
// для монохромной маски). Это - значения по умолчанию и не могут быть
// изменены. Их также необходимо восстановить после переноса

SetBkColor(hdcDest, RGB(255, 255, 255));	// все 1 --> 0xFFFFFF
SetTextColor(hdcDest, RGB(0, 0, 0));		// все 0 --> 0x000000
// Реальная работа
BitBlt(hdcDest, x, y, dx, dy, hdcSrc, x0, y0, SRCINVERT);
BitBlt(hdcDest, x, y, dx, dy, hdcMask, 0, 0, SRCAND);
BitBlt(hdcDest, x, y, dx, dy, hdcSrc, x0, y0, SRCINVERT);

При переносе выполняются следующие действия:

  1. Первый шаг (BitBlt со значением ROP, равным SRCINVERT) изменяет с помощью XOR биты приемника, используя биты источника. Это выглядит немного забавно, но второй XOR вернет картинку в исходное состояние.
  2. Второй шаг (BitBlt со значением SRCAND) - операция маскирования. При наложении с помощью операции AND маски на биты приемника все прозрачные пикселы оставляют изображение нетронутым, тогда как непрозрачные сбрасывают его в 0 (черный цвет). Теперь приемник содержит черные пикселы в непрозрачной области и инвертированные источником пикселы - в прозрачной.
  3. На третьем шаге (BitBlt со значением SRCINVERT) вновь биты источника накладыватся XOR на приемник. Прозрачные пикселы восстанавливаются в исходное состояние (после двух последовательных XOR), а непрозрачные копируются с источника (значение XOR 0 = значение).

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

Метод черного источника

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

// Подготовить приемник для монохромного переноса (необходимо только
// для монохромной маски). Это - значения по умолчанию и не могут быть
// изменены. Их также необходимо восстановить после переноса
SetBkColor(hdcDest, RGB(255, 255, 255));	// все 1 --> 0xFFFFFF
SetTextColor(hdcDest, RGB(0, 0, 0));		// все 0 --> 0x000000
// Реальная работа
BitBlt(hdcDest, x, y, dx, dy, hdcMask, 0, 0, SRCAND);
BitBlt(hdcDest, x, y, dx, dy, hdcSrc, x0, y0, SRCPAINT);

И вновь используется маска, чтобы заполнить черным цветом непрозрачные места и оставить оставшиеся пикселы нетронутыми. Затем источник накладывается на место назначения с помощью OR, рисуя на не-черных областях приемника. Так как в прозрачных местах источника содержатся только черные пикселы, операция OR оставляет приемник в этих местах нетронутым. Заметьте, что для второго BitBlt могла быть с успехом применена операция SRCINVERT вместо SRCPAINT. Предварительная подготовка источника устраняет возможность случая (1 XOR 1), в котором эти две операции отличаются.

Экранное мерцание при этом методе значительно менее заметно, и прозрачность выглядит очень хорошо, если Вы поместили черные пикселы в нужных местах источника. Это - тот самый механизм, который используется Windows для рисования иконок. Файлы .ICO состоят из двух частей, XOR-маски и самой картинки. Для растров таких малых размеров прозрачность достигается очень легко.

Растровая прозрачность

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

Построение маски

Создать монохромную маску из цветного растра довольно просто - встроенное в BitBlt преобразование проделает всю работу автоматически. Цель в том, чтобы в полученной маске все непрозрачные пикселы были установлены в 0, а прозрачные - в 1. Установив цвет фона равным прозрачному цвету, Вы именно это и проделаете. Нет необходимости устанавливать цвет текста, потому что он в преобразовании из цветного режима в монохромный не используется (все пикселы, отличные по цвету от фоновых, сбрасываются в 0). Это выполняет приведенный код:

SetBkColor(hdcSrc, rgbTransparent);
BitBlt(hdcMask, 0, 0, dx, dy, hdcSrc, x0, y0, SRCCOPY);

Он создает маску с единицами в тех местах, где пикселы источника имеют прозрачный цвет, и нулями - в остальных - то есть такую же, что мы использовали ранее.

Использование маски

Настало время применить описанные выше методы. Метод истинной маски не требует дополнительной работы: маска создана и источник не нуждается в изменениях. Три последовательных переноса вызывают мерцание, но их всего три.

Метод черного источника, с другой стороны, требует дополнительной работы над исходным растром - прозрачные биты нужно установить в 0. Конечно, если прозрачным цветом с самого начала является черный, растр уже готов к выводу. Сброс прозрачных пикселов в черный цвет на исходном растре очень похож на уже описанный сброс непрозрачных пикселов на приемнике. Он выполняется с использованием маски:

SetBkColor(hdcSrc, RGB(0,0,0));		// все 1 --> черный (0x000000)
SetTextColor(hdcSrc,RGB(255,255,255));	// все 0 --> белый (0xFFFFFF)
BitBlt(hdcSrc, x0, y0, dx, dy, hdcMask, 0, 0, SRCAND);

Заметьте, что для прозрачного отображения понадобится два переноса. Выполнив прозрачный перенос, мы должны восстановить источник в исходное цветовое состояние:

SetBkColor(hdcSrc, rgbTransparent);	// все 1 --> прозрачный цвет
SetTextColor(hdcSrc, RGB(0,0,0));	// все 0 --> черный (0x000000)
BitBlt(hdcSrc, x0, y0, dx, dy, hdcMask, 0, 0, SRCPAINT);

Так как необходимо затрагивать, а затем восстанавливать исходный растр, общее число битовых переносов достигло уже четырех. Это замедляет процесс, но так как 2 переноса выполняются в растр, находящийся в памяти, а не на экране, мерцание гораздо менее заметно, чем в методе истинной маски. Если исходный растр можно содержать с установленными в черный цвет прозрачными областями, то оба переноса становятся не нужны, и для вывода на экран требуется только два битовых переноса - это просто необходимо для анимации.

Простая растровая прозрачность

Некоторые драйверы устройств прямо поддерживвают прозрачность. Драйвер сообщает об этой способности с использованием бита C1_TRANSPARENT, возвращая его при вызове GetDeviceCaps с параметром CAPS1. Специальный режим фона NEWTRANSPARENT говорит о том, что последующие переносы бит являются прозрачными. Текущий цвет фона назначения при этом должен быть прозрачным. При наличии такой возможности в драйвере прозрачная отрисовка выполняется так:

// Пытаемся только если режим поддерживается
if(GetDeviceCaps(hdcDest, CAPS1) & C1_TRANSPARENT)
{
// Специальный режим прозрачного фона
    oldMode = SetBkMode(hdcDest, NEWTRANSPARENT);
    rgbBk =  SetBkColor(hdcDest, rgbTransparent);
// Простое копирование; прозрачность получится автоматически
    BitBlt(hdcDest, x, y, dx, dy, hdcSrc, x0, y0, SRCCOPY);
    SetBkColor(hdcDest, rgbBk);
    SetBkMode(hdcDest, oldMode);
}

Это, конечно упрощает жизнь программисту. К сожалению, этот режим в настоящее время поддерживается немногими драйверами устройств - те, что поставляются с Windows 3.1, его не поддерживают. Ситуация должна измениться к лучшему в ближайшем будущем.

ПРИМЕЧАНИЕ

Забудьте об этом. Константы CAPS1 и C1_TRANSPARENT убраны из Platform SDK. Режим NEWTRANSPARENT оставлен в mmsystem.h по всей видимости, по недосмотру. Чтобы узнать, как без проблем выводить прозрачные растры в новых версиях Windows, прочитайте в MSDN описание Image Lists и функции TransparentBlt, а также взгляните на статью "Прозрачность - это просто" на нашем сайте. - Прим. перев.

Прозрачность и DIB'ы

Если исходный растр является аппаратно-независимым (Device-Intependent Bitmap, DIB), весь процесс "маскировки" можно сильно упростить, используя его, и как источник, и как маску одновременно и манипулируя таблицей цветов. Этот процесс идентичен вышеописанному - кроме того, что приложение может выполнять цветовые преобразования, изменяя таблицу цветов, как в приведенном примере псевдокода:

// Сохранить копию таблицы цветов.
// Сохранить маску.
for (every color in the color table)
{
    if (color == rgbTransparent)
        color = white;
    else
        color = black;
}

// Подготовить приемник с помощью переноса маски.
StretchDIBits(hdcDest, lpDIB, SRCAND); // (Да, там есть еще параметры)
// Теперь подготовим "зачерненный" источник для маскированного переноса.
for(every color in the color table)
{
    if (color == white)  // (мы его изменяли ранее)
        color = black;
    else
        color = original color from color table;
}
// Выведем   приемник с эффектом прозрачности.
StretchDIBits(hdcDest, lpDIB, SRCPAINT); // (Да, там есть еще параметры)

// Восстановим первоначальную таблицу цветов.

Заметьте, что в данном способе требуется только одна копия растра - и для источника, и для маски прозрачности, так как используется преимущество в виде таблицы цветов. Однако остаются дополнительные расходы по преобразованию DIB в аппаратно-зависимый растр.

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

Как обработать нажатие Enter в edit box'е?
Автор: Игорь Вартанов

Начнем с того, что для обработки нажатия Enter необходимо, чтобы (в общем случае) окно редактирования ожидало этого нажатия (т.е. имело стиль ES_MULTILINE). В противном случае система выполнит трансляцию этого нажатия в нажатие кнопки родительского окна, имеющей в текущий момент стиль BS_DEFAULTPUSHBUTTON. Кстати, это довольно неплохая методика для диалога, содержащего единственное окно ввода и имеющего кнопку по-умолчанию OK. Если же диалог (или окно) имеет несколько окон ввода, и логика работы приложения подразумевает, что нажатие Enter означает окончание ввода в выбранном окне и перевод фокуса на следующее, то скорее всего вам подойдет нижеследующая методика.

Основной вариант

Демонстрационный проект EditDlg

WinAPI
ПРИМЕЧАНИЕ
Обратите внимание, окно редактирования должно иметь стиль ES_MULTILINE.

Основная идея состоит в подмене стандартной процедуры окна редактирования (т.н. subclassing) при инициализации окна диалога, и выполнение в новой процедуре обработки нажатия клавиши. В нашем примере при обнаружении нажатия Enter выполняется копирование текста окна в буфер текста и перевод фокуса на следующий контрол диалогового окна. Если же была нажата иная клавиша, выполняется вызов стандартной оконной процедуры для окон класса "edit".

#include <windows.h>
#include "resource.h"

WNDPROC oldEditProc = NULL;

LRESULT CALLBACK newEditProc(HWND hEdit, UINT msg, WPARAM wParam,
                                           LPARAM lParam)
{
    switch(msg)
    {
    case WM_KEYDOWN:
        {
            if(VK_RETURN == wParam)
            {
                HWND hParent = GetParent(hEdit);
                SendMessage( hParent, msg, wParam, lParam);
                SetFocus( GetNextDlgTabItem( hParent, hEdit, FALSE ) );
                return 0;   // запрет обработки по-умолчанию
            }
        }
        break;
    case WM_CHAR:
        if(VK_RETURN == wParam)
            return 0;       // запрет обработки по-умолчанию
        break;
    }
    return CallWindowProc(oldEditProc, hEdit, msg, wParam, lParam);
}

BOOL CALLBACK DlgProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
    static char m_edText[256] = "";

    switch(msg)
    {
    case WM_INITDIALOG:
        oldEditProc = (WNDPROC) SetWindowLong(
                                    GetDlgItem(hDlg, IDC_EDIT1),
                                    GWL_WNDPROC, (LONG)newEditProc);
        break;
    case WM_COMMAND:
        if(wParam == IDCANCEL)
            EndDialog(hDlg, 0);
        break;
    case WM_KEYDOWN:
        if( VK_RETURN == wParam)
            GetDlgItemText(hDlg, IDC_EDIT1, m_edText, 256);
        break;
    }
    return 0;
}

int WINAPI WinMain(  HINSTANCE hInstance, HINSTANCE hPrevInstance,
                     LPSTR     lpCmdLine, int       nCmdShow )
{
    DialogBox(hInstance, "MAINDLG", HWND_DESKTOP, (DLGPROC)DlgProc);
    return 0;
}

Обратите внимание на то, что обработчики сообщений при обнаружении нажатия Enter возвращают из оконной процедуры нуль. Это делается для того, чтобы сообщения не передавались обработчику по-умолчанию (и, следовательно, не выполнялось нажатие кнопки по-умолчанию).

MFC
ПРИМЕЧАНИЕ
Обратите внимание, окно редактирования должно иметь стиль ES_MULTILINE.

Для реализации поведения приложения, аналогичного только что описанному, необходимо создать класс, производный от CEdit, имеющий собственные обработчики сообщений WM_KEYDOWN и WM_CHAR (при создании класса и добавлении обработчиков используйте ClassWizard).

// .h-файл класса ////////////////////////////////////////////////

. . .

class CEnterEdit : public CEdit
{
public:
    CEnterEdit();
public:
    virtual ~CEnterEdit();
protected:
    //{{AFX_MSG(CEnterEdit)
    afx_msg void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags);
    afx_msg void OnChar(UINT nChar, UINT nRepCnt, UINT nFlags);
    //}}AFX_MSG
    DECLARE_MESSAGE_MAP()
};

// .cpp-файл класса //////////////////////////////////////////////

. . .

BEGIN_MESSAGE_MAP(CEnterEdit, CEdit)
    //{{AFX_MSG_MAP(CEnterEdit)
    ON_WM_KEYDOWN()
    ON_WM_CHAR()
    //}}AFX_MSG_MAP
END_MESSAGE_MAP()

void CEnterEdit::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
    if(nChar == VK_RETURN)
    {
        // Предполагаем, что родительское окно эдит-бокса -
        // диалог класса CEditDlgDlg, который имеет буфер хранения
        // введенного текста m_edText типа CString.

        CEditDlgDlg* pDlg = (CEditDlgDlg*) GetParent();
        GetWindowText(pDlg->m_edText);
        pDlg->GetNextDlgTabItem(this)->SetFocus();
        return;   // запрет обработки по-умолчанию
    }
    CEdit::OnKeyDown(nChar, nRepCnt, nFlags);
}

void CEnterEdit::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
    if(nChar == VK_RETURN)
        return;   // запрет обработки по-умолчанию
    CEdit::OnChar(nChar, nRepCnt, nFlags);
}
ПРИМЕЧАНИЕ
Подмена оконной процедуры - универсальный метод для получения необходимой функциональности. Если же есть возможность получить доступ к циклу сообщений, то можно воспользоваться альтернативной методикой - обработкой сообщения WM_KEYDOWN в самом цикле (см. далее - Альтернативный вариант).

Пример EditDlg демонстрирует обработку нажатия клавиши Enter. Он содержит два проекта - WinAPI и MFC.

Альтернативный вариант

Не всегда целесообразно обработку нажатия Enter возлагать на окно редактирования. Если в поведение приложения необходимо добавить указанную реакцию, но для самого окна достаточно обычной функциональности (однострочное окно редактирования), можно, не меняя стиля окна редактирования, самостоятельно обрабатывать нажатие Enter, анализируя содержимое сообщений в цикле обработки сообщений.

Необходимо помнить, что цикл обработки сообщений модального диалога реализуется самой системой и недоступен для программиста. В этом случае остается единственное средство - подмена оконной процедуры окна редактирования, описанная выше (см. Основной вариант).

Детали реализации этого метода очень сильно зависят от постановки задачи, среды разработки и организации цикла обработки сообщений. Общая схема такова:

  1. До выполнения DispacthMessage(&msg) необходимо проанализировать поле msg.message на приход сообщения WM_KEYDOWN.
  2. Если получено сообщение WM_KEYDOWN, и поле msg.wParam содержит VK_RETURN, то выполнить вызов функции-диспетчера нажатия Enter. При этом обычно необходимо избегать передачи полученного сообщения в функцию DispatchMessage(), чтобы не выполнялась обработка по-умолчанию.
  3. Для всех иных сообщений выполнить стандартную обработку.
MFC

Для программ, использующих MFC, все необходимые проверки выполняются в методе PreTranslateMessage() класса приложения или окна.

BOOL CMyWinApp::PreTranslateMessage ( MSG* pMsg )
{
    if(
        ( WM_KEYDOWN == pMsg->message ) &&
        ( VK_RETURN  == pMsg->wParam  )
      )
    {
        OnEnterPressed();  // вызов диспетчера нажатия Enter
        return TRUE;       // запрет дальнейшей обработки
    }
    // стандартная обработка сообщения
    return CWinApp::PreTranslateMessage ( pMsg );
}
WinAPI

Для приложений WinAPI реализация цикла обработки сообщений может выглядеть таким образом:

. . .
    while( GetMessage( &msg, NULL, 0, 0 ) )
    {
        if(
            ( WM_KEYDOWN == pMsg->message ) &&
            ( VK_RETURN  == pMsg->wParam  )
          )
        {
            OnEnterPressed();  // вызов диспетчера нажатия Enter
            continue;          // запрет дальнейшей обработки
        }
        // стандартная обработка сообщения
        TranslateMessage( &msg );
        DispatchMessage ( &msg );
    }
. . .

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

Редкий вариант, но вдруг вам понравится...
ПРИМЕЧАНИЕ
Поскольку этот вариант является существенным только для модальных диалогов, в которых, для того чтобы добраться до цикла сообщений, необходимо применить то (сабклассинг окна диалога) или иное (постановка локального хука) ухищрение, и поскольку сказанное совершенно не относится к MFC, где модальные диалоги "от системы" практически не применяются, то мы рассмотрим только WinAPI-вариант.
...локальный хук?

Условимся заранее, что теорию применения хуков вы получите из любых других источников ( например, из статьи Kyle Marsh Хуки в Win32 или Dr. Joseph M. Newcomer Хуки и DLL на нашем сайте). Там же вы познакомитесь и с их разновидностями. Мы же продолжим решать нашу задачу - перехват нажатия Enter в модальном диалоге.

Итак, в качестве необходимого теоретического минимума заметим, что механизм "крюков" (hook - англ., крюк) позволяет приложению зарегистрировать некий обработчик, который система будет вызывать в ответ на события, происходящие в ее недрах, с целью оповещения пользовательского кода об этих событиях. Локальный хук вызывается только для событий, относящихся к процессу, поставившему хук, что практически никак не ухудшает общую производительность системы вцелом. И потому именно этот механизм подходит нам для наших целей.

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

LRESULT DlgBoxMsgFilter( UINT code, WPARAM wParam, LPARAM lParam );

HHOOK g_hHook = NULL;

LRESULT CALLBACK HookProc( int code, WPARAM wParam, LPARAM lParam )
{
    LRESULT res = 0;

    // служебная обработка
    if( 0 > code )
        return CallNextHookEx( WH_MSGFILTER, code, wParam, lParam );

    // вызов пользовательской процедуры "полезного действия"
    res = DlgBoxMsgFilter( code, wParam, lParam );

    if( res > -1 )
        return res;
    return CallNextHookEx( WH_MSGFILTER, code, wParam, lParam );
}

BOOL CALLBACK DlgProc( HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam )
{
    switch( msg )
    {
    case WM_INITDIALOG:
        // постановка хука...
        g_hHook = SetWindowsHookEx( WH_MSGFILTER, HookProc,
                                    GetModuleHandle( NULL ),
                                    GetCurrentThreadId() );
        break;

    case WM_COMMAND:
        switch( LOWORD(wParam) )
        {
        case IDCANCEL:
            if( BN_CLICKED == HIWORD(wParam) )
            {
                // ... и его снятие
                if( g_hHook )
                    UnhookWindowsHookEx( h_hHook );
                EndDialog( hDlg, 0 );
            }
            break;
        }
        break;
    }
    return 0;
}

Теперь обратимся к процедуре . Легко заметить, что она выполняет практически те же действия, что и из ОСНОВНОГО ВАРИАНТА, а именно - обнаружение нажатия Enter и переход на следующий контрол, имеющий стиль . Поскольку нас интересуют только события диалогов (а не меню, и не скроллбаров), то и фильтровать мы будем только коды типа .

LRESULT DlgBoxMsgFilter( UINT code, WPARAM wParam, LPARAM lParam )
{
    LPMSG pMsg   = (LPMSG)lParam;
    HWND  hEdit1 = GetDlgItem( g_hDlg, IDC_EDIT1 ),
          hEdit2 = GetDlgItem( g_hDlg, IDC_EDIT2 );

    switch( code )
    {
    case MSGF_DIALOGBOX:
        {
            // следим за нажатиями в обоих эдитбоксах
            if( hEdit1 != pMsg->hwnd && hEdit2 != pMsg->hwnd )
                return -1;

            switch(pMsg->message)
            {
            case WM_KEYDOWN:
                if( VK_RETURN == pMsg->wParam )
                {
                    // нажат Enter, сообщим об этом родительскому окну (диалогу)
                    SendMessage( g_hDlg, pMsg->message, pMsg->wParam,
                                         pMsg->lParam );

                    // перейдем к следующему TABSTOP-контролу диалога
                    SetFocus( GetNextDlgTabItem( g_hDlg, pMsg->hwnd, FALSE ) );
                    return TRUE;
                }
                break;
            }
        }
        break;
    }
    return -1;
}

На этом, собственно, мы и остановимся. Насколько понятно/удобно/оправдано пользоваться этим методом - судить вам.

ПРИМЕЧАНИЕ
В демонстрационном проекте вы найдете подпроект HkEdDlg, в котором продемонстрирована приведенная методика. Там же, кстати, вы сможете найти и пример реализации глобального (системного) хука, но это, как говорится, уже совсем другая история...

Это все на сегодня. Пока!

Алекс Jenter jenter@rsdn.ru
Duisburg, 2001. Публикуемые в рассылке материалы принадлежат сайту RSDN.


ПРОГРАММИРОВАНИЕ НА V I S U A L C + +
РАССЫЛКА САЙТА
RSDN.RU
Выпуск No. 64 от 17 февраля 2002 г.

РАССЫЛКА ЯВЛЯЕТСЯ ЧАСТЬЮ ПРОЕКТА RSDN , НА САЙТЕ КОТОРОГО ВСЕГДА МОЖНО НАЙТИ ВСЮ НЕОБХОДИМУЮ РАЗРАБОТЧИКУ ИНФОРМАЦИЮ, СТАТЬИ, ФОРУМЫ, РЕСУРСЫ, ПОЛНЫЙ АРХИВ ПРЕДЫДУЩИХ ВЫПУСКОВ РАССЫЛКИ И МНОГОЕ ДРУГОЕ.

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

CТАТЬЯ

Заметка о производительности многопоточных Win32-программ
Автор: Роман Хациев

Тестовое приложение - 835 B

"Живые объекты"

Довольно давно я прочитал статью, автор которой объединил две концепции - многозадачность и объектно-ориентированное программирование. В результате получились так называемые "живые объекты". Идея крайне проста - при инициализации объекта создается отдельный поток и объект в нем живет своей жизнью, а создатель объекта по мере необходимости получает информацию о состоянии объекта из его свойств. Код для такого объекта на С++ выглядит примерно так:

class living_object
{
    ...
    static DWORD threadHelper(LPVOID);
    void run();
    public:
    bool animate();
    ...
};

bool living_object::animate()
{
    ...
    CreateThread(NULL, 0, ThreadHelper, (LPVOID)this, 0, &threadID);
    ...
}

DWORD living_object::threadHelper(LPVOID instance)
{
    ((living_object*)instance)->run();
}

void living_object::run()
{
    while(true)
    {
        ...
        Sleep(...);
    }
}

"Живые объекты" позволяют отказаться от необходимости делить внутреннюю логику объектов, работающих параллельно, на куски, которые создатель объектов должен сам вызывать в определенной последовательности, фактически беря на себя функции диспетчера потоков.

Конечно же нельзя забывать о том, что "живые объекты" привносят в программу проблемы синхронизации доступа к свойствам объекта. Особенно это относится к сложным типам данных наподобие std::vector. Однако было бы ошибкой думать, что базовые типы данных не нуждаются в синхронизации доступа к ним. Хотя на массово распространенных сейчас однопроцессорных системах подобное пренебрежение синхронизацией может не вызывать проблем, но на многопроцессорных системах последствия могут быть самыми неожиданными. Так что лучше не уподобляться тем программистам, которые были уверены, что их программы к 2000-му году уже не будут использоваться.

Немного теории

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

Что же вызывает переключение контекста? Вариантов всего два - или поток отдает управление операционной системе добровольно, посредством вызова одной из соответствующих функций, или операционная система сама отбирает управление у потока по истечении минимального разумного времени, называемого time slice.

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

  1. Каковы издержки на явное переключение контекста? Как они зависят от количества потоков в программе?
  2. Как влияет на производительность многопоточной программы наличие в системе дополнительного процессора?
  3. Как зависит производительность многопоточной программы от конкретной операционной системы?
  4. Какие существуют ограничения на количество потоков в программе?
Тестовая программа

Сразу хочу оговориться, что тестовая программа имитирует систему, активно использующую "живые объекты", описанные в начале заметки. Это связано с тем, что меня интересовали вышеперечисленные вопросы применительно именно к "живым объектам". Для многопоточных программ с другой логикой организации работы потоков результаты испытаний могут быть другие. Так же прошу принять во внимание, что замеры не проводились с лабораторной тщательностью, и поэтому нужно сделать скидку на определенную погрешность в цифрах. Для компиляции использовался Visual C++ 6 SP5.

Тестовая программа создает заданное количество "живых объектов", которые все вместе выполняют фиксированный объем вычислений, и замеряет общее время выполнения. Вычисления выглядят следующим образом - в цикле вызывается библиотечная функция rand(), результат которой делится по модулю на некоторое число. Каждый "живой объект" выполняет количество итераций цикла равное общему количеству итераций, заданному для всей программы, поделенному на количество "живых объектов".

Каждые сто итераций цикла "живой объект" вызывает функцию Sleep(0), которая фактически форсирует переключение контекста и передачу управления другому потоку.

ПРЕДУПРЕЖДЕНИЕ
Без вызова функции Sleep тестовая программа не отражала бы изменение реальных затрат времени на переключение контекста в зависимости от количества "живых объектов". В этом случае количество переключений контекста примерно равнялось бы продолжительности выполнения программы деленному на размер time slice независимо от количества потоков. И, следовательно, из-за того, что количество переключений контекстов фиксировано, увеличение времени исполнения очень слабо зависит от количества потоков (разница продолжительности выполнения между 2 и 4096 потоками составляет менее 300мс на 2xPIII-1000 под Windows 2000 Professional при общей продолжительности работы программы около 3200мс).

Тестовая программа была запущена на нескольких конфигурациях, оказавшихся под рукой. Для удобства сравнения результатов выбирались конфигурации с одинаковыми процессорами. Результаты сведены в следующую таблицу:

Кол-во потоков | 2xPIII-1000 Windows NT4 Server (мс|издержки) |  | 2xPIII-1000 Windows 2000 Professional (мс|издержки) |  | PIII-1000 Windows 2000 Professional (мс|издержки) |  | PIII-1000 Windows XP Professional (мс|издержки) |  | PIII-1000 Windows 98 SE (мс|издержки)
8192 | 8343 | 53% | 8391 | 57% | 16323 | 58% | 15913 | 50% |  |
4096 | 8500 | 56% | 8328 | 56% | 15172 | 47% | 14961 | 41% |  |
2048 | 8203 | 50% | 7937 | 49% | 14942 | 45% | 14792 | 40% |  |
1024 | 7843 | 44% | 7796 | 46% | 14731 | 43% | 14611 | 38% | 36776 | 208%
512 | 7562 | 39% | 7593 | 42% | 14431 | 40% | 14411 | 36% | 30632 | 156%
256 | 7547 | 38% | 7281 | 36% | 13620 | 32% | 14081 | 33% | 25273 | 112%
128 | 7328 | 34% | 7281 | 36% | 13619 | 32% | 13940 | 32% | 22971 | 92%
64 | 6671 | 22% | 6609 | 24% | 11917 | 16% | 12348 | 17% | 21254 | 78%
32 | 6547 | 20% | 6016 | 13% | 10616 | 3% | 10926 | 3% | 19911 | 67%
16 | 6000 | 10% | 5922 | 11% | 10515 | 2% | 10825 | 2% | 19323 | 62%
8 | 5984 | 10% | 5875 | 10% | 10515 | 2% | 10805 | 2% | 19184 | 61%
4 | 5968 | 9% | 5906 | 11% | 10515 | 2% | 10775 | 2% | 19124 | 60%
2 | 5453 | 0% | 5344 | 0% | 10415 | 1% | 10746 | 1% | 19087 | 60%
1 | 10703 |  | 10563 |  | 10315 | 0% | 10595 | 0% | 11943 | 0%
Анализ результатов

Проанализируем таблицу, чтобы получить ответы на наши вопросы.

Каковы издержки на явное переключение контекста? Как они зависят от количества потоков в программе?

Для двухпроцессорной системы два потока в программе дают оптимальную и максимально возможную производительность программы. Однако увеличение количества потоков приводит к постоянному увеличению времени выполнения программы на десять и более процентов. С другой стороны, на однопроцессорной системе увеличение количества потоков до 32 практически не сказывается на времени выполнения программы, после чего происходит резкий скачок на промежутке между 32 и 128 потоками, после чего рост продолжается более-менее плавно.

Сильно упрощая и обобщая результаты, можно сказать, что общие потери на переключение контекста процессора не будут превышать шестидесяти процентов на разумном количестве потоков (до 8192). Но даже такой упрощенный вывод необходимо уточнить:

  1. Тестовая программа создает стрессовую ситуацию - каждый "живой объект" стремится подмять под себя все свободное процессорное время, но мы заставляем его быть менее прожорливым, вызывая принудительное переключение контекста вызовом Sleep(0). Реальные "живые объекты" требуют меньше процессорного времени, что выражается в вызове функции Sleep с параметром, отличным от нуля. Таким образом в реальной программе переключения контекста будут происходить реже, чем в нашем тесте, и, следовательно, потери времени будут меньше.
  2. Наши рассуждения не относятся к линейке Windows 9x/ME. У операционных систем этого семейства отношения с многопоточностью крайне прохладные. Отсюда уточнение - если ваша программа будет активно использоваться под Windows 9x/ME, основательно работайте над эффективностью кода, так как потери во времени выполнения начинают превышать пятьдесят процентов уже после создания второго потока.
Как влияет на производительность многопоточной программы наличие в системе дополнительного процессора?

Для более наглядного представления данных упростим вышеприведенную таблицу, объединив данные для двух- и однопроцессорных систем и отбросив результаты, полученные под Windows 98SE:

Кол-во потоков | Двухпроцессорная система (мс) | Однопроцессорная система | Прирост производительности
8192 | 8367 | 16118 | 93%
4096 | 8414 | 15067 | 79%
2048 | 8070 | 14867 | 84%
1024 | 7820 | 14671 | 88%
512 | 7578 | 14421 | 90%
256 | 7414 | 13851 | 87%
128 | 7305 | 13780 | 89%
64 | 6640 | 12133 | 83%
32 | 6282 | 10771 | 71%
16 | 5961 | 10670 | 79%
8 | 5930 | 10660 | 80%
4 | 5937 | 10645 | 79%
2 | 5399 | 10581 | 96%
1 | 10633 | 10455 | -2%

То есть прирост производительности всегда меньше, чем в два раза, независимо от количества потоков. В среднем прирост производительности составляет порядка 85 процентов. Возможно (и даже наверняка) эта цифра будет отличаться для других процессоров, в особенности для линейки Intel Xeon, которая славится улучшенной поддержкой многопоточности, а так же для систем с количеством процессоров больше двух.

Как зависит производительность многопоточной программы от конкретной операционной системы?

Так как мы говорим о Win32, выбор операционных систем не очень велик - линейки Windows 9x/ME и Windows NT/2000/XP. Исходя из имеющихся данных, для Windows NT/2000/XP принципиальной разницы в производительности между всеми комбинациями NT/2000/XP и Workstation/Server нет, хотя, возможно, это будет опровергнуто дальнейшими испытаниями на других конфигурациях.

Результаты для Windows 98 SE говорят сами за себя. Ввиду того, что принципиальных изменений в ядро Windows 95 до сих пор внесено не было, можно смело утверждать, что эти результаты показательны для любой версии Windows 9x/ME.

Какие существуют ограничения на количество потоков в программе?

Разумно предположить, что количество потоков в процессе ограничено ресурсами самого процесса, а так же ресурсами ядра операционной системы.

ПРИМЕЧАНИЕ

Все сказанное ниже справедливо для линейки Windows NT/2000/XP.

Один из основных ресурсов ядра операционной системы, потребляемый при создании потока, это невыгружаемая памяти (non-paged memory) ядра. Создание одного потока требует около 12 килобайт невыгружаемой памяти. Ограничения на размер пула невыгружаемой памяти устанавливается в следующем ключе системного реестра:

HKLM\System\CurrentControlSet\Control\Session Manager\Memory Management

параметрами NonPagedPoolQuota и NonPagedPoolSize. Их значение по умолчанию равно нулю, что отдает управление этими значениями в руки операционной системы.

То есть при современных объемах памяти крайне сложно выбрать все ресурсы ядра операционной системы, создавая большое количество потоков. Остаются только ресурсы процесса, о чем мы и поговорим подробнее.

Как известно, каждому процессу выделяется адресное пространство в четыре гигабайта, но под свои нужды процесс может употребить только первые два гигабайта. Собственно из этих двух гигабайт и выделяется память под стек для вновь создаваемого потока. Размер стека определяется двумя факторами - параметром /STACK линковщика и параметром dwStackSize функции CreateThread.

Размер стека, заданный параметром dwStackSize, не может быть меньше, чем указано в параметре /STACK линковщика и по умолчанию равен ему. Размер стека, используемый линковщиком по умолчанию равен одному мегабайту. Таким образом максимальное количество потоков, которые можно создать при всех параметрах заданных по умолчанию, равняется примерно 2035. По достижении этого предела функция CreateThread начинает возвращать ошибку ERROR_NOT_ENOUGH_MEMORY, что является истинной правдой - если умножить количество потоков на размер стека по умолчанию, то как раз получается примерно два гигабайта - размер адресного пространства отданный процессу на карманные расходы.

Обойти это ограничение можно указав меньший размер стека параметром /STACK линковщика или в Project Settings (Link/Output/Stack Allocations/Reserve) в Microsoft Visual C++. Размер стека указывается в байтах. Меняя это значение надо быть осторожным ввиду того, что стек используется не только для хранения адресов возврата функций и передачи параметров, но и для хранения локальных переменных. Однако это тема отдельного разговора.

Заключение

"Живые объекты" предоставляют очень интересные возможности для построения сложных систем. И проведенные тесты дают нам возможность трезво и со значительной степенью точности оценить влияние этой технологии на производительность конечной программы. Потому что лично меня, как программиста, очень нервирует манипулирование категориями "быстро/медленно" или "будет тормозить/не будет тормозить" ;)

Отдельное спасибо хочется сказать Дэну Парновскому, который сделал ряд ценных замечаний в процессе разработки тестовой программы, без которых результаты измерений были бы некорректны.

Так же хочу поблагодарить Константина Князева, чьи комментарии помогли более четко сформулировать некоторые ключевые моменты заметки.

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

Как предоставить пользователю выбор источника данных для создания ADO Connection?
Автор: Марк Балонкин

Для определения источника данных во время выполнения существует DataLink диалог. Создать или отредактировать ADO Connection с помощью DataLink поможет IDataSourceLocator (OLE DB). Пример кода:

// DataLocator.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"

#import "C:\Program Files\Common Files\System\ado\msado21.tlb" \
        rename("EOF","ADOEOF") rename("BOF","ADOBOF")
#import "C:\Program Files\Common Files\System\ole db\Oledb32.dll"

int main(int argc, char* argv[])
{
    CoInitialize(NULL);
    MSDASC::IDataSourceLocatorPtr dl=NULL;
    ADODB::_ConnectionPtr pConn=NULL;
    try
    {
        dl.CreateInstance(__uuidof(MSDASC::DataLinks));
        pConn=dl->PromptNew();
        if(NULL==pConn)
            return -1;
        pConn->Open(pConn->ConnectionString, L"", L"", -1 );
    }
    catch (_com_error&)
    {
        return -1;
    }
    CoUninitialize();
    return 0;
}

Это все на сегодня. Пока!

Алекс Jenter jenter@rsdn.ru
Duisburg, 2001. Публикуемые в рассылке материалы принадлежат сайту RSDN.


ПРОГРАММИРОВАНИЕ НА V I S U A L C + +
РАССЫЛКА САЙТА
RSDN.RU
Выпуск No. 65 от 24 февраля 2002 г.

РАССЫЛКА ЯВЛЯЕТСЯ ЧАСТЬЮ ПРОЕКТА RSDN , НА САЙТЕ КОТОРОГО ВСЕГДА МОЖНО НАЙТИ ВСЮ НЕОБХОДИМУЮ РАЗРАБОТЧИКУ ИНФОРМАЦИЮ, СТАТЬИ, ФОРУМЫ, РЕСУРСЫ, ПОЛНЫЙ АРХИВ ПРЕДЫДУЩИХ ВЫПУСКОВ РАССЫЛКИ И МНОГОЕ ДРУГОЕ.

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

CТАТЬЯ

Взаимодействие .NET с неуправляемым кодом
Автор: Алифанов Андрей

Демонстрационный проект

Введение

Первоначально цель написания данной статьи заключалась в следующем: показать, как писать обертки для низкоуровневых интерфейсов на языках семейства VisualStudio 7.0. Но по мере знакомства с предметом я понял, что тему можно расширить, так как схожие механизмы используются не только для взаимодействия с COM-объектами, но и для взаимодействия с низкоуровневым системным кодом Windows, в частности - с Win32 API. Кроме того, я думаю, что многим будет интересно узнать, как же в действительности выглядит код, который создается утилитами типа TlbImp (я здесь имею в виду код на языке C#, а не реально создающийся код на MSIL).

Эта тема достаточно актуальна для переходного периода, когда существует огромное количество кода, написанного с использованием Win32 API и COM-объектов, с которым нужно взаимодействовать. Проблема несколько смягчается, если используются объекты, описанные в библиотеках типов, за счет использования утилит, автоматически генерирующих сборки. Но что делать, если библиотеки типов нет или код находится в экспортируемой функции некоторой динамической библиотеки? В этом случае выход только один - вручную написать необходимые обертки.

Механизм, используемый для взаимодействия .NET с неуправляемым кодом, достаточно хорошо должен быть знаком тем, кто описывал интерфейсы на языке IDL. В обоих случаях используются атрибуты.

Атрибуты главным образом используются для правильного обмена данными между управляемым (managed) и неуправляемым (unmanaged) кодом, но не только.

PlatformInvoke

Рассмотрение интеграции управляемого и неуправляемого кода начнем с PlatformInvoke. Эта технология позволяет достаточно просто вызывать функции динамических библиотек путем отображения объявления статического метода на точку входа PE/COFF.

Чтобы указать, что метод определен во внешней DLL, нужно пометить его как extern и использовать атрибут метода System.Runtime.InteropServices.DllImport. Этот атрибут сообщает CLR, что описание метода и дополнительные параметры (если они есть) необходимо использовать как информацию для вызова LoadLibrary и GetProcAddress, перед тем, как вызвать метод.

Атрибут DllImport имеет ряд параметров, которые можно опустить, но имя файла должно быть задано всегда. Это имя используется CLR для вызова LoadLibrary. Имя функции, которую необходимо вызвать из DLL, задается или прямым заданием параметра EntryPoint атрибута DllImport, или берется из описания самой функции. Во втором случае подразумевается, что ее название в программе соответствует ее имени в библиотеке. Пример использования этого атрибута приведен ниже:

[DllImport("KERNEL32.DLL", EntryPoint="MoveFileW",  SetLastError=true,
           CharSet=CharSet.Unicode, ExactSpelling=true,
           CallingConvention=CallingConvention.StdCall)]
public static extern bool MoveFile(String src, String dst);

Это все, что касается только технологии PlatformInvoke. Темы, рассматриваемые дальше, имеют отношение как к PlatformInvoke, так и к общению с COM-объектами из .NET. За исключением, естественно, описаний интерфейсов и классов.

Конвертирование типов

Важный вопрос, встающий при взаимодействии управляемого и неуправляемого кода: конвертирование типов. При осуществлении вызова функции ее параметры одновременно являются экземплярами и CLR, и внешнего мира. Здесь важно понимать, что каждый параметр имеет два типа - управляемый и неуправляемый. Кроме того, некоторые типы имеют одинаковый вид и в управляемом, и в неуправляемом коде, а это значит, что при их передаче никакого преобразования не требуется. К таким типам относятся следующие: Single, Double, SByte, Byte, Int16, UInt16, Int32, UInt32, Int64, UInt64 и одномерные массивы этих типов. Все остальные типы должны преобразовываться.

Для задания правил конвертирования используется атрибут MarshalAs. Он может применяться к параметрам и результатам методов, полям структур и классов. Этот атрибут не является обязательным, так как каждый тип данных имеет встроенные правила маршалинга. Но если данный тип может быть сконвертирован во множество других типов, необходимо применение этого атрибута.

Полное описание этого атрибута можно найти в документации, дальше по тексту будут даны некоторые наиболее интересные в использовании примеры.

Для примеров я взял реализацию COM-объекта <Менеджера компонентных категорий>, а именно структуры CATEGORYINFO и интерфейсов IEnumGUID, IEnumCATEGORYINFO и ICatInformation.

Описание структур - атрибут StructLayout

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

Поле Pack этого атрибута может иметь следующие значения:

  • Sequential - в этом случае данные будут расположены в памяти последовательно в порядке их объявления.
  • Explicit - в этом случае можно управлять точным расположением каждого члена структуры с помощью задания дополнительного атрибута FieldOffset для каждого поля.
  • CharSet - задает правила маршалинга строковых данных и может принимать следующие значения:
  • Ansi - строки передаются в виде 1-байтовых ANSI символов
  • Auto - строки автоматически конвертируются в зависимости от системы (Unicode в WindowsNT и ANSI в Windows9x)
  • None = Ansi
  • Unicode - строки передаются в виде 2-байтовых символов.

Пример использования атрибутов StructLayout и MarshalAs приведен ниже:

IDL

#define CATDESC_MAX 128
typedef struct tagCATEGORYINFO {
  CATID   catid;
  LCID    lcid;
  OLECHAR szDescription[CATDESC_MAX];
} CATEGORYINFO, *LPCATEGORYINFO;

C#

[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
public struct CATEGORYINFO
{
  public Guid catid;
  public uint lcid;
  [MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)]
  public String szDescription;
};

Можно видеть, что в данном случае строка szDescription передается в виде массива фиксированной длины из Unicode-символов. Для маршалинга остальных полей применяются правила по умолчанию.

ПРИМЕЧАНИЕ

В документации Miscrosoft утверждается, что поле SizeConst атрибута MarshalAs задает размер массива в байтах, на самом деле это поле задает количество элементов в массиве.

Обработка ошибок в COM и .NET

COM методы сообщают об ошибках, возвращая соответствующий HRESULT, .NET методы - генерируя исключения. Здесь возникает одна проблема - .NET игнорирует любые положительные значения HRESULT, что приводит к неправильной работе перечислителей типа IEnumXXX, так как последние сигнализируют о достижении конца последовательности возвратом значения S_FALSE = 1. Чтобы решить эту проблему - для методов введен атрибут PreserveSig. Задание этого атрибута позволяет подавить генерацию исключений .NET, и гарантирует возврат точного значения HRESULT из COM метода, в противном случае результатом метода всегда будет S_OK = 0. Пример использования этого атрибута приведен ниже.

Описание интерфейсов - атрибуты ComImport, Guid, InterfaceType

Для описания интерфейсов и классов применяются атрибуты ComImport и Guid. Атрибут ComImport - показывает, что тип был ранее определен в COM. CLR обращается с такими типами не так, как с <родными>, в частности - по другому создает объекты таких типов, выполняет приведение типов, удержание объектов в памяти и т.д. Этот атрибут обязательно сопровождается атрибутом Guid, название которого говорит само за себя.

Атрибут InterfaceType применяется для описания базового COM интерфейса и может принимать следующие значения: дуальный, IDispatch или IUnknown. Если этот атрибут опущен, то считается, что интерфейс дуальный. В нашем случае все интерфейсы наследуют от IUnknown.

Описания параметров методов - атрибуты In, Out, MarshalAs

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

Для примера рассмотрим метод ICatInformation.GetCategoryDesc.

void ICatInformation.GetCategoryDesc([In] ref Guid rcatid,
                                     [In] uint lcid,
                                     [Out, MarshalAs(UnmanagedType.LPWStr)]

                                       out String pszDesc);

ПРИМЕЧАНИЕ

Данный метод можно описать в виде функции:

[return : MarshalAs(UnmanagedType.LPWStr)] String ICatInformation.GetCategoryDesc([In] ref Guid rcatid, [In] uint lcid);

Такой синтаксис можно использовать для функций Win32API и методов COM-интерфейсов, имеющих последний параметр типа out и возвращающих HRESULT. Далее, в примерах интерфейсов и в демонстрационном приложении методы будут записываться подобным образом. Модификатор return нужен только при задании атрибута MarshalAs для методов COM-интерфейсов.

Если посмотреть на IDL-описание этого метода, видно, что передается ссылка на CLSID (GUID), по правилам языка C# структуры передаются по значению, а Guid является именно структурой. Поэтому, чтобы правильно передать параметр в COM метод, мало задать атрибут [In], нужно еще указать ключевое слово ref для параметра rcatid. Точно также, для задания выходных параметров нужно не только задавать атрибут [Out], но и ключевое слово out. При несоблюдении этих правил возможны ошибки компиляции или, что хуже, ошибки времени выполнения.

Атрибут MarshalAs задает правила передачи параметров, наиболее часто он используется в следующих видах:

  • MarshalAs(UnmanagedType.LPWStr) - Unicode-строка. Память под строку распределяется и освобождается через системные функции.
  • MarshalAs(UnmanagedType.LPArray, SizeParamIndex=n) - передается одномерный массив, размер массива задается параметром с номером n, нумерация параметров начинается с нуля.
  • MarshalAs(UnmanagedType.Interface) - передается COM интерфейс.

Примеры интерфейсов

IDL

[
  object,
  uuid(0002E000-0000-0000-C000-000000000046),
  pointer_default(unique)
]
interface IEnumGUID : IUnknown
{
  HRESULT Next([in] ULONG celt,
               [out, size_is(celt), length_is(*pceltFetched)] GUID *rgelt,
               [out] ULONG *pceltFetched);

  HRESULT Skip([in] ULONG celt);

  HRESULT Reset();

  HRESULT Clone([out] IEnumGUID **ppenum);
}

[
  object,
  uuid(0002E011-0000-0000-C000-000000000046),
  pointer_default(unique)
]
interface IEnumCATEGORYINFO : IUnknown
{
  HRESULT Next([in] ULONG celt,
               [out, size_is(celt), length_is(*pceltFetched)] CATEGORYINFO *rgelt,
               [out] ULONG *pceltFetched);

  HRESULT Skip([in] ULONG celt);

  HRESULT Reset();

  HRESULT Clone([out] IEnumCATEGORYINFO **ppenum);
}

[
  object,
  uuid(0002E013-0000-0000-C000-000000000046),
  pointer_default(unique)
]
interface ICatInformation : IUnknown
{
  HRESULT EnumCategories(
    [in] LCID lcid,
    [out] IEnumCATEGORYINFO** ppenumCategoryInfo);

  HRESULT GetCategoryDesc(
    [in] REFCATID rcatid,
    [in] LCID lcid,
    [out] LPWSTR* pszDesc);

  [local]
  HRESULT EnumClassesOfCategories(
    [in] ULONG cImplemented,
    [in,size_is(cImplemented)] CATID rgcatidImpl[],
    [in] ULONG cRequired,
    [in,size_is(cRequired)] CATID rgcatidReq[],
    [out] IEnumCLSID** ppenumClsid);

  [call_as(EnumClassesOfCategories)]
  HRESULT RemoteEnumClassesOfCategories(
    [in] ULONG cImplemented,
    [in,unique,size_is(cImplemented)] CATID rgcatidImpl[],
    [in] ULONG cRequired,
    [in,unique,size_is(cRequired)] CATID rgcatidReq[],
    [out] IEnumCLSID** ppenumClsid);

  [local]
  HRESULT IsClassOfCategories(
    [in] REFCLSID rclsid,
    [in] ULONG cImplemented,
    [in,size_is(cImplemented)] CATID rgcatidImpl[],
    [in] ULONG cRequired,
    [in,size_is(cRequired)] CATID rgcatidReq[]);

  [call_as(IsClassOfCategories)]
  HRESULT RemoteIsClassOfCategories(
    [in] REFCLSID rclsid,
    [in] ULONG cImplemented,
    [in,unique,size_is(cImplemented)] CATID rgcatidImpl[],
    [in] ULONG cRequired,
    [in,unique,size_is(cRequired)] CATID rgcatidReq[]);

  HRESULT EnumImplCategoriesOfClass(
    [in] REFCLSID rclsid,
    [out] IEnumCATID** ppenumCatid);

  HRESULT EnumReqCategoriesOfClass(
    [in] REFCLSID rclsid,
    [out] IEnumCATID** ppenumCatid);
}

C#

[
  ComImport,
  Guid("0002E000-0000-0000-C000-000000000046"),
  InterfaceType(ComInterfaceType.InterfaceIsIUnknown)
]
public interface IEnumGUID
{
  [PreserveSig()]
  int Next([In] uint celt,
           [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex=0)] Guid[] rgelt,
           [Out] out uint pceltFetched);

  [PreserveSig()]
  int Skip([In] uint celt);

  void Reset();

  [return : MarshalAs(UnmanagedType.Interface)]
  IEnumGUID Clone();
};

[
  ComImport,
  Guid("0002E011-0000-0000-C000-000000000046"),
  InterfaceType(ComInterfaceType.InterfaceIsIUnknown)
]
public interface IEnumCATEGORYINFO
{
  [PreserveSig()]
  int Next([In] uint celt,
           [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex=0)] CATEGORYINFO[] rgelt,
           [Out] out uint pceltFetched);

  [PreserveSig()]
  int Skip([In] uint celt);

  void Reset();

  [return : MarshalAs(UnmanagedType.Interface)]
  IEnumCATEGORYINFO Clone();
};

[
  ComImport,
  Guid("0002E013-0000-0000-C000-000000000046"),
  InterfaceType(ComInterfaceType.InterfaceIsIUnknown)
]
public interface ICatInformation
{
  [return : MarshalAs(UnmanagedType.Interface)]
  IEnumCATEGORYINFO EnumCategories([In] uint lcid);

  [return : MarshalAs(UnmanagedType.LPWStr)]
  String GetCategoryDesc([In] ref Guid rcatid, [In] uint lcid);

  [return : MarshalAs(UnmanagedType.Interface)]
  IEnumGUID EnumClassesOfCategories([In] uint cImplemented,
                                    [In, MarshalAs(UnmanagedType.LPArray,

                                      SizeParamIndex=0)] Guid[] rgcatidImpl,
                                    [In] uint cRequired,
                                    [In, MarshalAs(UnmanagedType.LPArray,

                                      SizeParamIndex=2)] Guid[] rgcatidReq);

  [return : MarshalAs(UnmanagedType.Interface)]
  IEnumGUID RemoteEnumClassesOfCategories([In] uint cImplemented,
                                          [In, MarshalAs(UnmanagedType.LPArray,

                                            SizeParamIndex=0)] Guid[] rgcatidImpl,
                                          [In] uint cRequired,
                                          [In, MarshalAs(UnmanagedType.LPArray,

                                            SizeParamIndex=2)] Guid[] rgcatidReq);

  [PreserveSig()]
  int IsClassOfCategories([In] ref Guid rclsid,
                          [In] uint cImplemented,
                          [In, MarshalAs(UnmanagedType.LPArray,

                            SizeParamIndex=0)] Guid[] rgcatidImpl,
                          [In] uint cRequired,
                          [In, MarshalAs(UnmanagedType.LPArray,

                            SizeParamIndex=2)] Guid[] rgcatidReq);

  [PreserveSig()]
  int RemoteIsClassOfCategories([In] ref Guid rclsid,
                                [In] uint cImplemented,
                                [In, MarshalAs(UnmanagedType.LPArray,

                                  SizeParamIndex=0)] Guid[] rgcatidImpl,
                                [In] uint cRequired,
                                [In, MarshalAs(UnmanagedType.LPArray,

                                  SizeParamIndex=2)] Guid[] rgcatidReq);

  [return : MarshalAs(UnmanagedType.Interface)]
  IEnumGUID EnumImplCategoriesOfClass([In] ref Guid rclsid);

  [return : MarshalAs(UnmanagedType.Interface)]
  IEnumGUID EnumReqCategoriesOfClass([In] ref Guid rclsid);
};

Описание классов

Для описания классов также используются атрибуты ComImport и Guid. Классы с атрибутом ComImport не могут иметь никаких данных и методов.

Пример описания класса

IDL

Описание отсутствует

C#

[
  ComImport,
  Guid("0002E005-0000-0000-C000-000000000046")
]
public class StdComponentCategoriesMgr
{
};

Пример использования класса

C#

using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using ComCatWrapper;

public class Test
{
  static void Main()
  {
    StdComponentCategoriesMgr mgr = new StdComponentCategoriesMgr();
    ICatInformation catInfoItf = (ICatInformation)mgr;
    IEnumCATEGORYINFO enumCInfoItf = сatInfoItf.EnumCategories(0);

    // и т.д.

  }
}

Из этого примера видна еще одна особенность работы с COM-объектами в .NET: вместо привычного CoCreateInstance используется оператор new, а вместо QueryInterface используется приведение типов.

Демонстрационное приложение

Демонстрационное приложение, демонстрирующее работу с COM-интерфейсами, написано на C#. Проект состоит из двух модулей: модуля, обеспечивающего интерфейс пользователя (файл MainForm.cs) и модуля, содержащего обертки COM-объекта <Менеджер категорий компонентов> (файл ComCatWrapper.cs).

Как уже упоминалось, в файле ComCatWrapper.cs содержатся описания структуры CATEGORYINFO и интерфейсов IEnumGUID, IEnumCATEGORYINFO и ICatInformation, а также кокласса StdComponentCategoriesMgr.

Файл MainForm.cs содержит код, необходимый для построения простейшего пользовательского интерфейса и использует интерфейсы из ComCatWrapper.cs.

Первоначально список категорий пуст, он заполняется при нажатии кнопки <Заполнить>. Так сделано из-за того, что заполнение идет достаточно долго, а использование, например, дополнительных потоков усложнило бы логику программы.

Вся работа с COM-интерфейсами ведется в двух функциях: FillBtn_Click и FillNodes. Эти функции просты и достаточно подробно прокомментированы.

Визуально категории компонентов представляются в виде дерева следующего вида: описание категории, соответствующий ей идентификатор (CATID) и идентификаторы классов (CLSID), реализующих данную категорию.

Ниже приведен пример работы тестового приложения, использующего обертки COM-интерфейсов

Заключение

Как видим, обеспечить взаимодействие COM и .NET довольно просто для программиста на C#. Нужно только знать, какие параметры и как передавать между управляемым и неуправляемым кодом.

К сожалению, во время подготовки статьи выяснилось, что ManagedC++ и VB.NET не позволяют писать обертки для COM-объектов без использования tlb. Задание атрибута ComImport в этих языках приводит к выбрасыванию исключений при попытке создания экземпляров классов во время выполнения программы, хотя компиляция проходит без проблем. Что это - ошибка или так было задумано, я не знаю. В то же время классы-обертки, написанные на C#, можно использовать и из ManagedC++ и VB.NET.

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

Как подменить функцию API?
Автор: Павел Блудов

Рисунок: Приложение HookAPI

Демонстрационное приложение (WTL Dialog) HookAPI (100kb) Требует наличия звуковой карты. Методы 3, 4 и 5 не будут работать под windows9x/ME.

Рисунок: Приложение HookAPI2

Демонстрационное приложение (WTL Dialog) HookAPI2 (20kb) Требует наличия WinSockets 1.0.

Переопределение с помощью препроцессора

#include <windows.h>

WINUSERAPI BOOL WINAPI MyMessageBeep(IN UINT uType)
{
    //Your code here
}

#define MessageBeep MyMessageBeep

Теперь если в коде программы встретится MessageBeep препроцессор заменит ее на нашу MyMessageBeep. Очень просто.

Но что если хочется добавить немного своей логики в уже откомпилированный код, изменить работу чужой библиотеки, пересобрать которую нет никакой возможности? Иными словами, заставить уже откомпилированный код вызвать нашу функцию вместо стандартной. Это вполне реально. Давайте поближе рассмотрим, как под windows процедуры одного модуля используют процедуры другого.

Модификация таблиц импорта/экспорта

Весь API, доступный из какого-либо модуля, описан в так называемой таблице экспорта этого модуля. С другой стороны, список API, необходимый для нормальной работы опять-таки, любого модуля, находится в его таблице импорта.

Код вызова процедуры из другого модуля выглядит примерно так:

call        dword ptr [__imp__MessageBeep@4 (004404cc)]

И, если изменить значение по этому адресу, можно подменить оригинальнкю функцию свей. Для этого нам понадобится:

  • Отыскать таблицу импорта функций для нужного нам модуля
  • Отыскать там указатель на перехватываемую функцию
  • Снять с этого участка памяти утрибут ReadOnly
  • Записать указатель на нашу функцию
  • Вернуть защиту обратно

HRESULT ApiHijackImports(
                HMODULE hModule,
                LPSTR szVictim,
                LPSTR szEntry,
                LPVOID pHijacker,
                LPVOID *ppOrig
                )
{
    // Check args
    if (::IsBadStringPtrA(szVictim, -1) ||
        (!IS_INTRESOURCE(szEntry) && ::IsBadStringPtrA(szEntry, -1)) ||
        ::IsBadCodePtr(FARPROC(pHijacker)))
    {
        return E_INVALIDARG;
    }

    PIMAGE_DOS_HEADER pDosHeader = PIMAGE_DOS_HEADER(hModule);

    if (::IsBadReadPtr(pDosHeader, sizeof(IMAGE_DOS_HEADER)) ||
        IMAGE_DOS_SIGNATURE != pDosHeader->e_magic)
    {
        return E_INVALIDARG;
    }

    PIMAGE_NT_HEADERS pNTHeaders =
        MakePtr(PIMAGE_NT_HEADERS, hModule, pDosHeader->e_lfanew);

    if (::IsBadReadPtr(pNTHeaders, sizeof(IMAGE_NT_HEADERS)) ||
        IMAGE_NT_SIGNATURE != pNTHeaders->Signature)
    {
        return E_INVALIDARG;
    }

    HRESULT hr = E_UNEXPECTED;

    // Locate the victim
    IMAGE_DATA_DIRECTORY& impDir =
        pNTHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
    PIMAGE_IMPORT_DESCRIPTOR pImpDesc =
        MakePtr(PIMAGE_IMPORT_DESCRIPTOR, hModule, impDir.VirtualAddress),
        pEnd = pImpDesc + impDir.Size / sizeof(IMAGE_IMPORT_DESCRIPTOR) - 1;

    while(pImpDesc < pEnd)
    {
        if (0 == ::lstrcmpiA(MakePtr(LPSTR, hModule, pImpDesc->Name), szVictim))
        {
            if (0 == pImpDesc->OriginalFirstThunk)
            {
                // no import names table
                return E_UNEXPECTED;
            }

            // Locate the entry
            PIMAGE_THUNK_DATA pNamesTable =
                MakePtr(PIMAGE_THUNK_DATA, hModule, pImpDesc->OriginalFirstThunk);

            if (IS_INTRESOURCE(szEntry))
            {
                // By ordinal
                while(pNamesTable->u1.AddressOfData)
                {
                    if (IMAGE_SNAP_BY_ORDINAL(pNamesTable->u1.Ordinal) &&
                        WORD(szEntry) == IMAGE_ORDINAL(pNamesTable->u1.Ordinal))
                    {
                        hr = S_OK;
                        break;
                    }
                    pNamesTable++;
                }
            }
            else
            {
                // By name
                while(pNamesTable->u1.AddressOfData)
                {
                    if (!IMAGE_SNAP_BY_ORDINAL(pNamesTable->u1.Ordinal))
                    {
                        PIMAGE_IMPORT_BY_NAME pName = MakePtr(PIMAGE_IMPORT_BY_NAME,
                                            hModule, pNamesTable->u1.AddressOfData);

                        if (0 == ::lstrcmpiA(LPSTR(pName->Name), szEntry))
                        {
                            hr = S_OK;
                            break;
                        }
                    }
                    pNamesTable++;
                }
            }

            if (SUCCEEDED(hr))
            {
                // Get address
                LPVOID *pProc = MakePtr(LPVOID *, pNamesTable,
                            pImpDesc->FirstThunk - pImpDesc->OriginalFirstThunk);

                // Save original handler
                if (ppOrig)
                    *ppOrig = *pProc;

                // write to write-protected memory
                return WriteProtectedMemory(pProc, &pHijacker, sizeof(LPVOID));
            }
            break;
        }
        pImpDesc++;
    }
    return hr;
}

HRESULT WriteProtectedMemory(LPVOID pDest, LPCVOID pSrc, DWORD dwSize)
{
    // Make it writable
    DWORD dwOldProtect = 0;
    if (::VirtualProtect(pDest, dwSize, PAGE_READWRITE, &dwOldProtect))
    {
        ::MoveMemory(pDest, pSrc, dwSize);

        // Restore protection
        ::VirtualProtect(pDest, dwSize, dwOldProtect, &dwOldProtect);
        return S_OK;
    }

    return HRESULT_FROM_WIN32(GetLastError());
}

Впрочем, такой способ не будет работать если используется позднее связывание (delay load) или связывание во время исполнения (run-time load) с помощью ::GetProcAddress(). Это можно побороть если перехватить саму ::GetProcAddress(), и подменять возвращяемое значение при необходимости. А можно и подправить таблицу экспорта аналогичным способом:

HRESULT ApiHijackExports(
                HMODULE hModule,
                LPSTR szEntry,
                LPVOID pHijacker,
                LPVOID *ppOrig
                )
{
    // Check args
    if ((!IS_INTRESOURCE(szEntry) && ::IsBadStringPtrA(szEntry, -1))
        || ::IsBadCodePtr(FARPROC(pHijacker)))
    {
        return E_INVALIDARG;
    }

    PIMAGE_DOS_HEADER pDosHeader = PIMAGE_DOS_HEADER(hModule);

    if (::IsBadReadPtr(pDosHeader, sizeof(IMAGE_DOS_HEADER)) ||
        IMAGE_DOS_SIGNATURE != pDosHeader->e_magic)
    {
        return E_INVALIDARG;
    }

    PIMAGE_NT_HEADERS pNTHeaders =
        MakePtr(PIMAGE_NT_HEADERS, hModule, pDosHeader->e_lfanew);

    if (::IsBadReadPtr(pNTHeaders, sizeof(IMAGE_NT_HEADERS)) ||
        IMAGE_NT_SIGNATURE != pNTHeaders->Signature)
    {
        return E_INVALIDARG;
    }

    HRESULT hr = E_UNEXPECTED;

    IMAGE_DATA_DIRECTORY& expDir =
        pNTHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
    PIMAGE_EXPORT_DIRECTORY pExpDir =
        MakePtr(PIMAGE_EXPORT_DIRECTORY, hModule, expDir.VirtualAddress);

    LPDWORD pdwAddrs = MakePtr(LPDWORD, hModule, pExpDir->AddressOfFunctions);
    LPWORD pdwOrd = MakePtr(LPWORD, hModule, pExpDir->AddressOfNameOrdinals);
    DWORD dwAddrIndex = -1;

    if (IS_INTRESOURCE(szEntry))
    {
        // By ordinal

        dwAddrIndex = WORD(szEntry) - pExpDir->Base;
        hr = S_OK;
    }
    else
    {
        // By name
        LPDWORD pdwNames = MakePtr(LPDWORD, hModule, pExpDir->AddressOfNames);
        for (DWORD iName = 0; iName < pExpDir->NumberOfNames; iName++)
        {
            if (0 == ::lstrcmpiA(MakePtr(LPSTR, hModule, pdwNames[iName]), szEntry))
            {
                dwAddrIndex = pdwOrd[iName];
                hr = S_OK;
                break;
            }
        }
    }

    if (SUCCEEDED(hr))
    {
        if (pdwAddrs[dwAddrIndex] >= expDir.VirtualAddress &&
            pdwAddrs[dwAddrIndex] < expDir.VirtualAddress + expDir.Size)
        {
            // We have a redirection
            LPSTR azRedir = MakePtr(LPSTR, hModule, pdwAddrs[dwAddrIndex]);
            ATLASSERT(!IsBadStringPtrA(azRedir, -1));

            LPSTR azDot = strchr(azRedir, '.');
            int nLen = azDot - azRedir;
            LPSTR azModule = (LPSTR)alloca(nLen);
            memcpy(azModule, azRedir, nLen);
            azModule[nLen] = '\x0';

            // Try to patch redirected function
            return ApiHijackExports(
                ::GetModuleHandle(azModule), azDot + 1, pHijacker, ppOrig);
        }

        if (ppOrig)
            *ppOrig = MakePtr(LPVOID, hModule, pdwAddrs[dwAddrIndex]);

        DWORD dwOffset = DWORD_PTR(pHijacker) - DWORD_PTR(hModule);

        // write to write-protected memory
        hr = WriteProtectedMemory(pdwAddrs + dwAddrIndex, &dwOffset, sizeof(LPVOID));
    }

    return hr;
}

Имейте в виду, под Windows9x нельзя честно подменить экспорты для разделяемых библиотек, таких как user32.dll, kernel32.dll и gdi32.dll. Это связано с тем, что область памяти начиная с адреса 7FC00000h и выше совместно используестя всеми процессами в системе, и модификация сказалась бы на каждом из них. А это нежелательно, поскольку память, занимаемая нашей функцией-перехватчиком, наоборот, принадлежит только нашему процессу. Во всех остальных процессах в системе ::GetProcAddress(), после подмены таблицы экспорта, вернула бы неправильный указатель. Тем не менее, если нельзя, но очень хочется, то можно. Для этого нам придется вручную создать новый дескриптор в GDT (вот тут-то у Windows9x проблем не возникает) и используя этот дескриптор произвести необходимые изменения. Но будьте готовы к тому, что понадобится написать свою разделяемую библиотеку, установить ее в системе и проверять ID процесса при каждом обращении. Рабочий пример есть на internals.com.

Модификация самого обработчика

Эти два способа работают в 99% случаев. Последний процент - это подмена функции, вызываемой внутри чужого модуля, т.е. когда и вызаваемая и вызывающая процедура находятся в одном и том же, да к тому же чужом, модуле. В этом случае, вызов будет сделан напрямик, а не через таблицы импорта/экспорта. Тут уже ничего сделать нельзя. Почти. Можно изменить саму функцию-обработчик, с тем чтобы перенаправить вызовы в нашу собственную. Делается это довольно просто: в начало исходного обработчика прописывается команда безусловного перехода на нашу процедуру, а если нужно вызвать оригинал, то нужно просто сохранить первые 5 байт затертых командой перехода, добавить после них опять-таки команду безусловного перехода на изначальный код +5 байт. Разумется, эти пять байт кода не дожны содержать команд перехода или вызова. Кроме того, может понадобиться больше чем 5 байт, ведь команда перехода посреди длинной инструкции работать не будет. Это случается крайне редко. Обычно код функции, как его генерит компилятор для I86 выглядит примерно так: инициализация стека, загрузка в регистры параметров функции, их проверка и переход в случае неудовлетворительных результатов. Этого вполне хватает чтобы вставить наш маленький перехватчик. Но бывает и так:

CSomeClass::Release:
FF152410E475           call    dword ptr [InterlockedDecrement]
85C0                   test    eax,eax

Или даже

CSomeClass::NonImplemented:
C20400                 ret     4

Что, впрочем, можно распознать и вернуть код ошибки если инструкции ret, jmp или call встретится слишком рано. Но вот такой случай распознать не получится:

SomeFunction:
33C0                   xor     eax,eax
SomeFunction2:
55                     push    ebp
8BEC                   mov     ebp,esp

Иными словами, модификация SomeFunction приведет к неизвестным изменениям в SomeFunction2, и, возможно, краху всей системы.

Все это сильно усложняет нам задачу. Нужно дизассемблировать эти байты и проверить каждую инструкцию. Чтобы немного облегчить нам жизнь, фирма Майкрософт разработала специальный SDK для такого рода трюков: Microsoft Detours. С этим SDK задача подмены чужой функции реализуется удивительно просто:

#include <detours.h>

DetourFunction(PBYTE(::MessageBeep), PBYTE(MyMessageBeep));

После чего все вызовы ::MessageBeep(), откуда бы они не были произведены, окажутся вызовами нашей MyMessageBeep(). Что и требовалось.

Модификация самого обработчика 2

Довольно оригинальный вариант предыдущего способа был предложен Дмитрием Крупорницким: первая инструкция перехватываемой функции заменяется инструкцией прерывания INT 3. Далее процедура обработки необработанных исключений (unhandled exception handler) подменяет регистр EIP на адрес нашей функции-перехватчика.

static DWORD_PTR m_dwFunction;

static LONG WINAPI MyUnhandledExceptionFilter(PEXCEPTION_POINTERS pExceptionInfo)
{
    if (pExceptionInfo->ContextRecord->Eip != m_dwFunction)
        return EXCEPTION_CONTINUE_SEARCH;

    // Continue execution from MyMessageBeep
    pExceptionInfo->ContextRecord->Eip = (DWORD_PTR)MyMessageBeep;
    return EXCEPTION_CONTINUE_EXECUTION;
}

LRESULT CMainDlg::OnMethod5(WORD /*wNotifyCode*/, WORD wID,
   HWND /*hWndCtl*/, BOOL& /*bHandled*/)
{
    m_dwFunction = (DWORD_PTR)::GetProcAddress(::GetModuleHandle("USER32.dll"),
 "MessageBeep");
    BYTE nSavedByte = *(LPBYTE)m_dwFunction;
    LPTOP_LEVEL_EXCEPTION_FILTER pOldFilter = <
     BR >
       ::SetUnhandledExceptionFilter(MyUnhandledExceptionFilter);

    const BYTE nInt3 = 0xCC;
    // Inject int 3
    HRESULT hr = WriteProtectedMemory(LPVOID(m_dwFunction), &nInt3, sizeof(const BYTE));
    if (SUCCEEDED(hr))
    {
        ::MessageBeep(m_uType);

        // Restore function
        hr = WriteProtectedMemory(LPVOID(m_dwFunction), &nSavedByte, sizeof(BYTE));
    }

    ::SetUnhandledExceptionFilter(pOldFilter);

    return 0;
}

Недостатком такого способа является его непредсказуемость. Кто угодно может зарегистрировать свой обработчик исключений и поломать нам логику. Более того, инструкции (...)/(), часто встречающиеся в программах, могут перехватить управление и не дать нашему обработчику шанса.

Подмена с использованием оберток(wrappers)

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

А вот с COM-объектами и интерфейсами обертки работают как нельзя лучше. Для этого создается другой COM-объект, реализующий нужный нам интерфейс, к нему создатся аггрегированный оригинальный COM-объект, а перехватчик отдается тем кто с ним будет в дальнейшем работать. Если оригинальный COM-объект не поддерживает аггрегацию, то придется реализовать все его интерфейсы, и если у него нету никаких внутренних (недокументированных) интерфейсов, то, возможно, все и заработает.

Все это касается подмены API текущего процесса. Если понадобится перехватить вызов в чужом процессе, то следует выбрать любой из приведенных здесь методов, поместить их в DLL и закинуть ее в чужой процесс. Это тема для отдельного разговора и здесь рассматриваться не будет.

Использованные статьи и литература
  • Форматы PE и COFF файлов
  • Расширение MSGINA - это просто
  • API Spying Techniques for Windows 9x, NT and 2000
  • APIHijack
  • EliCZ ApiHooks
  • Контроль вызова API функций в среде систем Windows '95, Windows '98 и Windows NT

Это все на сегодня. Пока!

Алекс Jenter jenter@rsdn.ru
Duisburg, 2001. Публикуемые в рассылке материалы принадлежат сайту RSDN.


ПРОГРАММИРОВАНИЕ НА V I S U A L C + +
РАССЫЛКА САЙТА
RSDN.RU
Выпуск No. 66 от 3 марта 2002 г.

РАССЫЛКА ЯВЛЯЕТСЯ ЧАСТЬЮ ПРОЕКТА RSDN , НА САЙТЕ КОТОРОГО ВСЕГДА МОЖНО НАЙТИ ВСЮ НЕОБХОДИМУЮ РАЗРАБОТЧИКУ ИНФОРМАЦИЮ, СТАТЬИ, ФОРУМЫ, РЕСУРСЫ, ПОЛНЫЙ АРХИВ ПРЕДЫДУЩИХ ВЫПУСКОВ РАССЫЛКИ И МНОГОЕ ДРУГОЕ.

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

Помнится, когда-то я уже публиковал статью, посвященную вопросу многозадачности и синхронизации потоков (нитей). Сегодня я предлагаю вам вернуться к этой теме, но уже на более подробном уровне. Вместе с Павлом Блудовым мы подробно рассмотрим один из объектов синхронизации - критические секции, и причем не просто их применение, но и их внутреннее устройство.

CТАТЬЯ

Критические секции
Автор: Paul Bludov

Демонстрационный проект CSTest (7.8kb)
Файл csdbg.h (1.8kb)
Файл csdbg2.h (2.5kb)
Классы-обертки для критических секций cswrap.h (0.5kb)

Введение

Критические секции - это объекты, используемые для блокироки доступа к некоторорым важным данным всем нитям (threads) приложения, кроме одной, в один момент времени. Например, имеется переменная m_pObject и несколько нитей, вызывающих методы объекта, на который ссылается m_pObject. Причем эта переменная может изменять свое значение время от времени. Иногда там даже оказывается нуль. Предположим, имеется вот такой код:

// Нить ?1
void Proc1()
{
    if (m_pObject)
        m_pObject->SomeMethod();
}

// Нить #2
void Proc2(IObject *pNewObject)
{
    if (m_pObject)
        delete m_pObject;
    m_pObject = pNewobject;
}

Тут мы имеем потенциальную опасность вызова m_pObject->SomeMethod() после того, как объект был уничтожен при помощи delete m_pObject. Дело в том, что в системах с вытесняющей многозадачностью выполнение любой нити процесса может прерваться в самый неподходящий для нее момент времени и начнет выполняться совершенно другая нить. В данном примере неподходящим моментом будет тот, в котором нить #1 уже проверила m_pObject, но еще не успела вызвать SomeMethod(). Выполнение нити #1 прервалось, и начала исполняться нить #2. Причем нить #2 успела вызвать деструктор объекта. Что же произойдет, когда нить #1 получит немного процессорного времени и вызовет-таки SomeMethod() у уже несуществующего объекта? Наверняка что-то ужасное.

Именно тут приходят на помощь критические секции. Перепишем наш пример.

// Нить #1
void Proc1()
{
    ::EnterCriticalSection(&m_lockObject);
    if (m_pObject)
        m_pObject->SomeMethod();
    ::LeaveCriticalSection(&m_lockObject);
}

// Нить #2
void Proc2(IObject *pNewObject)
{
    ::EnterCriticalSection(&m_lockObject);
    if (m_pObject)
        delete m_pObject;
    m_pObject = pNewobject;
    ::LeaveCriticalSection(&m_lockObject);
}

Код, помещенный между ::EnterCriticalSection() и ::LeaveCriticalSection() с одной и той же критической секцией в качестве параметра, никогда не будет выполняться параллельно. Это означает, что если нить #1 успела "захватить" критическую секцию m_lockObject, то при попытке нити #2 заполучить эту же критическую секцию в свое единоличное пользование, ее выполнение будет приостановлено до тех пор, пока нить #1 не "отпустит" m_lockObject при помощи вызова ::LeaveCriticalSection(). И наоборот, если нить #2 успела раньше нити #1, то та "подождет", прежде чем начнет работу с m_pObject.

Работа с критическими секциями

Что же происходит внутри критических секций и как они устроены? Прежде всего, следует отметить, что критические секции это не объекты ядра операционной системы. Практически вся работа с критическими секциями происходит в создавшем их процессе. Их этого следует, что критические секции могут быть использованы только для синхронизации в пределах одного процесса. Теперь рассмотрим критические секции поближе.

Структура RTL_CRITICAL_SECTION

typedef struct _RTL_CRITICAL_SECTION {
    PRTL_CRITICAL_SECTION_DEBUG DebugInfo; // Используется операционной системой
    LONG LockCount;        // Счетчик использования этой критической секции
    LONG RecursionCount;    // Счетцик повторного захвата из нити-владельца
    HANDLE OwningThread;    // Уникальный ID нити-владельца
    HANDLE LockSemaphore;    // Объект ядра используемый для ожидания
    ULONG_PTR SpinCount;    // Количество холостых циклов перед вызовом ядра
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;

Поле LockCount увеличивается на единицу при каждом вызове ::EnterCriticalSection() и уменьшается при каждом вызове ::LeaveCriticalSection(). Это первая (а часто и единственная проверка) на пути к "захвату" критической секции. Если после увеличения в этом поле находится ноль, это означает, что до этого момента непарных вызовов ::EnterCriticalSection() из других ниток не было. В этом случае можно забрать данные, охраняемые этой критической секцией в монопольное пользование. Таким образом, если критическая секция интенсивно используется не более чем одной нитью, ::EnterCriticalSection() практически вырождается в ++LockCount, а ::LeaveCriticalSection() в --LockCount. Это очень важно. Это означает, что использование многих тысяч критических секций в одном процессе не повлечет значительного расхода ни системных ресурсов, ни процессорного времени.

СОВЕТ

Не стоит экономить на критических секциях. Много наэкономить все равно не получится.

В поле RecursionCount хранится количество повторных вызовов ::EnterCriticalSection() из одной и той же нити. Действительно, если вызвать ::EnterCriticalSection() из одной и той же нити несколько раз, все вызовы будут успешны. Т.е. вот такой код не останосится навечно во втором вызове ::EnterCriticalSection(), а отработает до конца.

// Нить #1
void Proc1()
{
    ::EnterCriticalSection(&m_lock);
    // ...
    Proc2()
    // ...
    ::LeaveCriticalSection(&m_lock);
}

// Все еще нить #1
void Proc2()
{
    ::EnterCriticalSection(&m_lock);
    // ...
    ::LeaveCriticalSection(&m_lock);
}

Действительно, критические секции предназначены для защиты данных от доступа из нескольких ниток. Многократное использование одной и той же критической секции из одной нити не приведет к ошибке. Это вполне нормальное явление. Следите, чтобы количество вызовов ::EnterCriticalSection() и ::LeaveCriticalSection() совпадало, и все будет хорошо.

Поле OwningThread содержит 0 для никем не занятых критических секций или уникальный идентификатор нити-владельца. Это поле проверяется, если при вызове ::EnterCriticalSection() поле LockCount, после увеличения на единицу, оказалось больше нуля. Если OwningThread совпадает с уникальным идентификатором текущей нити, то RecursionCount просто увеличивается на единицу и ::EnterCriticalSection() возвращается немедленно. Иначе ::EnterCriticalSection() будет дожидаться, пока нить, владеющая критической секцией, не вызовет ::LeaveCriticalSection() необходимое количество раз.

Поле LockSemaphore используется, если нужно подождать, пока критическая секция освободится. Если LockCount больше нуля и OwningThread не совпадает с уникальным идентификатором текущей нити, то ждущая нить создает объект ядра (событие) и вызывает ::WaitForSingleObject(LockSemaphore). Нить-владелец, после уменьшения RecursionCount, проверяет его, и если значение этого поля равно нулю, а LockCount больше нуля, то это значит, что есть как минимум одна нить, ожидающая, пока LockSemaphore не окажется в состоянии "случилось!". Для этого нить-владелец вызывает ::SetEvent() и какая-то одна (только одна) из ожидающих ниток пробуждается и получает доступ к критическим данным.

WindowsNT/2k генерирует исключение, если попытка создать событие не увенчалась успехом. Это верно как для функций ::Enter/LeaveCriticalSection() так и для ::InitializeCriticalSectionAndSpinCount() с установленным старшим битом параметра SpinCount. Но только не WindowsXP. Разработчики ядра этой операционной системы поступили по-другому. Вместо генерации исключения, функции ::Enter/LeaveCriticalSection(), если не могут создать собственное событие, начинают использовать заранее созданный глобальный объект. Один на всех. Таким образом, в случае катастрофической нехватки системных ресурсов, программа под управлением WindowsXP ковыляет какое-то время дальше. Действительно, писать программы, способные продолжать работать после того, как ::EnterCriticalSection() сгенерировала исключение, черезвычайно сложно. Как правило, если программистом и предусмотрен такой поворот событий, то дальше вывода сообщения об ошибке и аварийного завершеня программы дело не идет. Как следствие, WindowsXP игнорирует старший бит поля LockCount.

И, наконец, поле SpinCount. Это поле используется только многопроцессорными системами. В однопроцессорных системах, если критическая секция занята другой нитью, можно только переключить управление на нее и подождать наступления нашего события. В многопроцессорных системах есть альтернатива: прогнать некоторое количество раз холостой цикл, проверяя каждый раз, не освободилась ли наша критическая секция. Если за SpinCount раз это не получилось, переходим к ожиданию. Это гораздо эффективнее, чем переключение на планировщик ядра и обратно. Кроме того, в WindowsNT/2k старший бит этого поля служит для индикации того, что объект ядра, хендл которого находится в поле LockSemaphore, должен быть создан заранее. Если системных ресурсов для этого недостаточно, система сгенерирует исключение, и программа может "урезать" свою функциональнось. Или совсем завершить работу.

API для работы с критическими секциями

BOOL InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

BOOL InitializeCriticalSectionAndSpinCount(LPCRITICAL_SECTION lpCriticalSection, DWORD dwSpinCount);

Заполняют поля структуры, адресуемой lpCriticalSection. После вызова любой из этих функций критическая секция готова к работе.

Листинг 1. Псевдокод RtlInitializeCriticalSection из ntdll.dll

VOID RtlInitializeCriticalSection(LPRTL_CRITICAL_SECTION pcs)
{
    RtlInitializeCriticalSectionAndSpinCount(pcs, 0)
}
VOID RtlInitializeCriticalSectionAndSpinCount(LPRTL_CRITICAL_SECTION pcs,
   DWORD dwSpinCount)
{
    pcs->DebugInfo = NULL;
    pcs->LockCount = -1;
    pcs->RecursionCount = 0;
    pcs->OwningThread = 0;
    pcs->LockSemaphore = NULL;
    pcs->SpinCount = dwSpinCount;
    if (0x80000000 & dwSpinCount)
        _CriticalSectionGetEvent(pcs);

}

DWORD SetCriticalSectionSpinCount(LPCRITICAL_SECTION lpCriticalSection, DWORD dwSpinCount);

Устанавливает значение поля SpinCount и возвращает его предыдущее значение. Напоминаю, что старший бит отвечает за "привязку" события, используемого для ожидания доступа к данной критической секции.

Листинг 2. Псевдокод RtlSetCriticalSectionSpinCount из ntdll.dll

DWORD RtlSetCriticalSectionSpinCount(LPRTL_CRITICAL_SECTION pcs, DWORD dwSpinCount)
{
    DWORD dwRet = pcs->SpinCount;
    pcs->SpinCount = dwSpinCount;
    return dwRet;
}

VOID DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

Освобождает ресурсы, занимаемые критической секцией.

Листинг 3. Псевдокод RtlDeleteCriticalSection из ntdll.dll

VOID RtlDeleteCriticalSection(LPRTL_CRITICAL_SECTION pcs)
{
    pcs->DebugInfo = NULL;
    pcs->LockCount = -1;
    pcs->RecursionCount = 0;
    pcs->OwningThread = 0;
    if (pcs->LockSemaphore)
    {
        ::CloseHandle(pcs->LockSemaphore);
        pcs->LockSemaphore = NULL;
    }
}

VOID EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

BOOL TryEnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

Осуществляют "захват" критической секции. Если критическая секция занята другой нитью, то ::EnterCriticalSection() будет ждать, пока та освободится, а ::TryEnterCriticalSection() вернет FALSE.

Листинг 4. Псевдокод RtlEnterCriticalSection из ntdll.dll

VOID RtlEnterCriticalSection(LPRTL_CRITICAL_SECTION pcs)
{
    if (::InterlockedIncrement(&pcs->LockCount))
    {
        if (pcs->OwningThread == (HANDLE)::GetCurrentThreadId())
        {
            pcs->RecursionCount++;
            return;
        }

        RtlpWaitForCriticalSection(pcs);
    }

    pcs->OwningThread = (HANDLE)::GetCurrentThreadId();
    pcs->RecursionCount = 1;
}

BOOL RtlTryEnterCriticalSection(LPRTL_CRITICAL_SECTION pcs)
{
    if (-1L == ::InterlockedCompareExchange(&pcs->LockCount, 0, -1))
    {
        pcs->OwningThread = (HANDLE)::GetCurrentThreadId();
        pcs->RecursionCount = 1;
    }
    else if (pcs->OwningThread == (HANDLE)::GetCurrentThreadId())
    {
        ::InterlockedIncrement(&pcs->LockCount);
        pcs->RecursionCount++;
    }
    else
        return FALSE;

    return TRUE;
}

VOID LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

Освобождает критическую секцию,

Листинг 5. Псевдокод RtlLeaveCriticalSection из ntdll.dll

VOID RtlLeaveCriticalSectionDbg(LPRTL_CRITICAL_SECTION pcs)
{
    if (--pcs->RecursionCount)
        ::InterlockedDecrement(&pcs->LockCount);
    else if (::InterlockedDecrement(&pcs->LockCount) >= 0)
        RtlpUnWaitCriticalSection(pcs);
}

Классы-обертки для критических секций

Листинг 6. Код классов CLock, CAutoLock и CScopeLock

class CLock
{
    friend class CScopeLock;
    CRITICAL_SECTION m_CS;
public:
    void Init() { ::InitializeCriticalSection(&m_CS); }
    void Term() { ::DeleteCriticalSection(&m_CS); }

    void Lock() { ::EnterCriticalSection(&m_CS); }
    BOOL TryLock() { return ::TryEnterCriticalSection(&m_CS); }
    void Unlock() { ::LeaveCriticalSection(&m_CS); }
};

class CAutoLock : public CLock
{
public:
    CAutoLock() { Init(); }
    ~CAutoLock() { Term(); }
};

class CScopeLock
{
    LPCRITICAL_SECTION m_pCS;
public:
    CScopeLock(LPCRITICAL_SECTION pCS) : m_pCS(pCS) { Lock(); }
    CScopeLock(CLock& lock) : m_pCS(&lock.m_CS) { Lock(); }
    ~CScopeLock() { Unlock(); }

    void Lock() { ::EnterCriticalSection(m_pCS); }
    void Unlock() { ::LeaveCriticalSection(m_pCS); }
};

Классы CLock и CAutoLock удобно использовать для синхронизации доступа к переменным класса, а CScopeLock предназначен, в основном, для использования в процедурах. Удобно, что компилятор сам позаботится о вызове ::LeaveCriticalSection() через наш деструктор.

Листинг 7. Пример использования CScopeLock

CAutoLock m_lockObject;
CObject *m_pObject;

void Proc1()
{
    CScopeLock lock(m_ lockObject); // Вызов lock.Lock();
    if (!m_pObject)
        return; // Вызов lock.Unlock();
    m_pObject->SomeMethod();

    // Вызов lock.Unlock();
}

Отладка критических секций

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

Ошибки, связанные с реализацией

Это довольно легко обнаруживаемые ошибки, как правило, связанные с непарностью вызовов ::EnterCriticalSection() и ::LeaveCriticalSection().

Листинг 8. Пропущен вызов ::EnterCriticalSection()

// Процедура предполагает, что m_lockObject.Lock(); уже был вызван
void Pool()
{
    for (int i = 0; i < m_vectSinks.size(); i++)
    {
        m_lockObject.Unlock();
        m_vectSinks[i]->DoSomething();
        m_lockObject.Lock();
    }
}

::LeaveCriticalSection() без ::EnterCriticalSection() приведет к тому, что первый же вызов ::EnterCriticalSection() остановит выполнение нити навсегда.

Листинг 9. Пропущен вызов ::LeaveCriticalSection()

void Proc()
{
    m_lockObject.Lock();
    if (!m_pObject)
        return;
    // ...
    m_lockObject.Unlock();
}

В этом примере, конечно, имеет смысл воспользоваться классом типа CScopeLock.

Кроме того, случается, что ::EnterCriticalSection() вызывается без инициализации критической секции с помощью ::InitializeCriticalSection(). Особенно часто такое случается с проектами, написанными с помощью ATL. Причем в debug-версии все работает замечательно, а release-версия рушится. Это происходит из-за так называемой "минимальной" CRT (_ATL_MIN_CRT), которая не вызывает конструкторы статических объектов (Q166480, Q165076). В ATL версии 7.0 эту проблему решили.

Еще я встречал такую ошибку: программист пользовался классом типа CScopeLock, но для экономии места называл эту переменную одной буквой:

    CScopeLock l(m_lock);

и как-то раз просто пропустил имя у переменной. Получилось

    CScopeLock (m_lock);

а что это означает? Компилятор честно сделал вызов конструктора CScopeLock, и тут же уничтожил этот безымянный объект, как и положено по стандарту. Т.е. сразу же после вызова метода Lock() последовал вызов Unlock(), и синхронизация перестала иметь место. Вообще, давать переменным, даже локальным, имена из одной буквы - путь быстрого наступления на всяческие грабли.

СОВЕТ

Если у Вас в процедуре больше одного цикла, то вместо int i,j,k стоит все-таки использовать что-то вроде int nObject, nSection, nRow.
Архитектурные ошибки

Самая известная из них это блокировка (deadlock) когда две нити пытаются захватить две или более критических секций, причем делают это в разном порядке.

Листинг 10. Взаимоблокировка двух ниток

void Proc1()
// Нить #1
{
    ::EnterCriticalSection(&m_lock1);
    // ...
        ::EnterCriticalSection(&m_lock2);
        // ...
        ::LeaveCriticalSection(&m_lock2);
    // ...
    ::LeaveCriticalSection(&m_lock1);
}

// Нить #2
void Proc2()
{
    ::EnterCriticalSection(&m_lock2);
    // ...
        ::EnterCriticalSection(&m_lock1);
        // ...
        ::LeaveCriticalSection(&m_lock1);
    // ...
    ::LeaveCriticalSection(&m_lock2);
}

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

CRITICAL_SECTION sec1;
CRITICAL_SECTION sec2;
// ...
sec1 = sec2;

Из такого присвоения трудно извлечь какую-либо пользу. А вот такой код иногда пишут:

struct SData
{
    CLock m_lock;
    DWORD m_dwSmth;
} m_data;

void Proc1(SData& data)
{
    m_data = data;
}

и все бы хорошо, если бы у структуры SData был конструктор копирования, например такой:

SData(const SData data)
{
    CScopeLock lock(data.m_lock);
    m_dwSmth = data.m_dwSmth;
}

но нет, программист посчитал, что хватит за глаза простого копирования полей и, в результате, переменная m_lock была просто скопирована, хотя именно в этот момент из другой нити она была "захвачена" и значение поля LockCount у нее в этот момент больше либо равен нулю. После вызова ::LeaveCriticalSection() в той нити, у исходной переменной m_lock значение поля LockCount уменьшилось на единицу. А у скопированно переменной - осталось прежним. И любой вызов ::EnterCriticalSection() в этой нити никогда не вернется. Он будет вечно ждать неизвестно чего.

Это только цветочки. С ягодками Вы очень быстро столкнетесь, если попытаетесь написать что-нибудь действительно сложное. Например, ActiveX-объект в многопоточном подразделении (MTA), создаваемый из скрипта, запущенного из-под контейнера, размещенного в однопоточном подразделении (STA). Ни слова не понятно? Не беда. Сейчас я попытаюсь выразить проблему более понятным языком. Итак. Имеется объект, вызывающий методы другого объекта, причем живут они в разных нитях. Вызовы производятся синхронно. Т.е. объект #1 переключает выполнение на нить объекта #2, вызывает метод и переключается обратно на свою нить. При этом выполнение нити #1 приостановлено до тех пор, пока не отработает нить объекта #2. Теперь положим, объект #2 вызывает метод объекта #1 из своей нити. Получается, что управление вернулось в объект #1, но из нити объекта #2. Если объект #1 вызывал метод объекта #2, захватив какую-либо критическую секцию, то при вызове метода объекта #1 тот заблокирует сам себя при повторном входе в ту же критическую секцию.

Листинг 11. Самоблокировка средствами одного объекта

// Нить #1
void IObject1::Proc1()
{
    // Входим в критическую секцию объекта #1
    m_lockObject.Lock();
    // Вызываем метод объекта #2, происходит переключение на нить объекта #2
    m_pObject2->SomeMethod();
    // Сюда мы попадем только по возвращении из m_pObject2->SomeMethod()
    m_lockObject.Unlock();
}

// Нить #2
void IObject2::SomeMethod()
{
    // Вызываем метод объекта #1 из нити объекта #2
    m_pObject1->Proc2();
}

// Нить #2
void IObject1::Proc2()
{
    // Пытаемся войти в критическую секцию объекта #1
    m_lockObject.Lock();
    // Сюда мы не попадем никогда
    m_lockObject.Unlock();
}

Если бы в примере не было переключения нитей, все вызовы произошли бы в нити объекта #1, и никаких проблем не возникло. Сильно надуманный пример? Ничуть. Именно переключение ниток лежит в основе подразделений COM (apartments). А из этого следует одно очень, очень неприятное правило.

СОВЕТ
Избегайте вызовов каких бы то ни было объектов при захваченных критических секциях.

Помните пример из начала статьи? Так вот, он абсолютно неприемлем в подобных случаях. Его придется переделать на что-то вроде

Листинг 12. Простой пример, не подверженный самоблокировке

// Нить #1
void Proc1()
{
    m_lockObject.Lock();
    CComPtr<IObject> pObject(m_pObject); // вызов pObject->AddRef();
    m_lockObject.Unlock();

    if (pObject)
        pObject->SomeMethod();
}

// Нить #2
void Proc2(IObject *pNewObject)
{
    m_lockObject.Lock();
    m_pObject = pNewobject;
    m_lockObject.Unlock();
}

Доступ к объекту остался по-прежнему синхронизован, но вызов SomeMethod(); происходит вне критической секции. Победа? Почти. осталась одна маленькая деталь. Давайте посмотрим, что происходит в Proc2():

void Proc2(IObject *pNewObject)
{
    m_lockObject.Lock();
    if (m_pObject.p)
        m_pObject.p->Release();
    m_pObject.p = pNewobject;
    if (m_pObject.p)
        m_pObject.p->AddRef();
    m_lockObject.Unlock();
}

Очевидно, что вызовы m_pObject.p->AddRef(); и m_pObject.p->Release(); происходят внутри критической секции. И если вызов метода AddRef(), как правило, безвреден, то вызов метода Release() может оказаться последним вызовом Release(), и объект самоуничтожится. В методе FinalRelease() объекта #2 может быть все что угодно, например, освобождение объектов, живущих в других подразделениях. А это опять приведет к переключению ниток и может вызвать самоблокировку объекта #1 по уже известному сценарию. Придется воспользоваться той же техникой, что и в методе Proc1()

// Нить #2
void Proc2(IObject *pNewObject)
{
    CComPtr<IObject> pPrevObject;
    m_lockObject.Lock();
    pPrevObject.Attach(m_pObject.Detach());
    m_pObject = pNewobject;
    m_lockObject.Unlock();
    // pPrevObject.Release();
}

Теперь потенциально последний вызов IObject2::Release() будет осуществлен после выхода из критической секции. А присвоение нового значения по-прежнему синхронизовано с вызовом IObject2::SomeMethod() из нити #1.

Способы обнаружения ошибок

Сначала стоит обратить внимание на "официальный" способ обнаружения блокировок. Если бы кроме ::EnterCriticalSection() и ::TryEnterCtiticalSection() существовал бы еще и ::EnterCriticalSectionWithTimeout(), то достаточно было бы просто указать какое-нибудь резонное значение для интервала ожидания, например, 30 секунд. Если критическая секция не освободилась в течение указанного времени, то с очень большой вероятностью она не освободится никогда. Имеет смысл подключить отладчик и посмотреть, что же творится в соседних нитьх. Но увы. Никаких ::EnterCriticalSectionWithTimeout() в Win32 не предусмотрено. Вместо этого есть поле CriticalSectionDefaultTimeout в структуре IMAGE_LOAD_CONFIG_DIRECTORY32, которое всегда равно нулю и, судя по всему, не используется. Зато используется ключ в реестре "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\CriticalSectionTimeout", который по умолчанию равен 30 суткам, и по истечению этого времени в системный лог попадает строка "RTL: Enter Critical Section Timeout (2 minutes)\nRTL: Pid.Tid XXXX.YYYY, owner tid ZZZZ\nRTL: Re-Waiting\n". К тому же это верно только для систем WindowsNT/2k/XP и только с CheckedBuild. У вас установлен CheckedBuild? Нет? А зря. Вы теряете исключительную возможность увидеть эту замечательную строку.

Ну, а какие у нас альтернативы? Да, пожалуй, только одна. Не использовать API для работы с критическими секциями. Вместо них написать свои собственные. Пусть даже не такие обточенные напильником, как в WindowsNT. Не страшно. Нам это понадобится только в debug-конфигурациях. В release'ах мы будем продолжать использовать оригинальный API от Майкрософт. Для этого напишем несколько функций полностью совместимых по типам и количеству аргументов с "настоящим" API и добавим #define как у MFC для переопределения оператора new в debug-конфигурациях.

Листинг 14. Собственная реализация критических секций

#if defined(_DEBUG) && !defined(_NO_DEADLOCK_TRACE)

#define DEADLOCK_TIMEOUT 30000
#define CS_DEBUG 1

// Создаем на лету событие для операций ожидания,
// но никогда его не освобождаем. Так удобней для отладки
static inline HANDLE _CriticalSectionGetEvent(LPCRITICAL_SECTION pcs)
{
    HANDLE ret = pcs->LockSemaphore;
    if (!ret)
    {
        HANDLE sem = ::CreateEvent(NULL, false, false, NULL);
        ATLASSERT(sem);

        if (!(ret = (HANDLE)::InterlockedCompareExchangePointer(
                &pcs->LockSemaphore, sem, NULL)))
            ret = sem;
        else
            ::CloseHandle(sem); // Кто-то успел раньше
    }
    return ret;
}

// Ждем, пока критическая секция не освободится либо время ожидания
// будет превышено
static inline VOID _WaitForCriticalSectionDbg(LPCRITICAL_SECTION pcs)
{
    HANDLE sem = _CriticalSectionGetEvent(pcs);

    DWORD dwWait;
    do
    {
        dwWait = ::WaitForSingleObject(sem, DEADLOCK_TIMEOUT);
        if (WAIT_TIMEOUT == dwWait)
        {
            ATLTRACE("Critical section timeout (%u msec):"
                    " tid 0x%04X owner tid 0x%04X\n", DEADLOCK_TIMEOUT,
                    ::GetCurrentThreadId(), pcs->OwningThread);
        }
    }while(WAIT_TIMEOUT == dwWait);
    ATLASSERT(WAIT_OBJECT_0 == dwWait);
}

// Выставляем событие в активное состояние
static inline VOID _UnWaitCriticalSectionDbg(LPCRITICAL_SECTION pcs)
{
    HANDLE sem = _CriticalSectionGetEvent(pcs);
    BOOL b = ::SetEvent(sem);
    ATLASSERT(b);
}

// Заполучем критическую секцию в свое пользование
inline VOID EnterCriticalSectionDbg(LPCRITICAL_SECTION pcs)
{
    if (::InterlockedIncrement(&pcs->LockCount))
    {
        // LockCount стал больше нуля.
        // Проверяем идентификатор нити
        if (pcs->OwningThread == (HANDLE)::GetCurrentThreadId())
        {
            // Нить та же самая. Критическая секция наша.
            pcs->RecursionCount++;
            return;
        }

        // Критическая секция занята другой нитью.
        // Придется подождать
        _WaitForCriticalSectionDbg(pcs);
    }

    // Либо критическая секция была "свободна",
    // либо мы дождались. Сохраняем идентификатор текущей нити.
    pcs->OwningThread = (HANDLE)::GetCurrentThreadId();
    pcs->RecursionCount = 1;
}

// Заполучаем критическую секцию если она никем не занята
inline BOOL TryEnterCriticalSectionDbg(LPCRITICAL_SECTION pcs)
{
    if (-1L == ::InterlockedCompareExchange(&pcs->LockCount, 0, -1))
    {
        // Это первое обращение к критической секции
        pcs->OwningThread = (HANDLE)::GetCurrentThreadId();
        pcs->RecursionCount = 1;
    }
    else if (pcs->OwningThread == (HANDLE)::GetCurrentThreadId())
    {
        // Это не первое обращение, но из той же нити
        ::InterlockedIncrement(&pcs->LockCount);
        pcs->RecursionCount++;
    }
    else
        return FALSE; // Критическая секция занята другой нитью

    return TRUE;
}

// Освобождаем критическую секцию
inline VOID LeaveCriticalSectionDbg(LPCRITICAL_SECTION pcs)
{
    // Проверяем, чтобы идентификатор текущей нити совпадал
    // с идентификатор нити-влядельца.
    // Если это не так, скорее всего мы имеем дело с ошибкой
    ATLASSERT(pcs->OwningThread == (HANDLE)::GetCurrentThreadId());

    if (--pcs->RecursionCount)
    {
        // Не последний вызов из этой нити.
        // Уменьшаем значение поля LockCount
        ::InterlockedDecrement(&pcs->LockCount);
    }
    else
    {
        // Последний вызов. Нужно "разбудить" какую-либо
        // из ожидающих ниток, если таковые имеются
        ATLASSERT(NULL != pcs->OwningThread);

        pcs->OwningThread = NULL;
        if (::InterlockedDecrement(&pcs->LockCount) >= 0)
        {
            // Имеется, как минимум, одна ожидающая нить
            _UnWaitCriticalSectionDbg(pcs);
        }
    }
}

// Удостоверяемся, что ::EnterCriticalSection() была вызвана
// до вызова этого метода
inline BOOL CheckCriticalSection(LPCRITICAL_SECTION pcs)
{
    return pcs->LockCount >= 0
        && pcs->OwningThread == (HANDLE)::GetCurrentThreadId();
}

// Переопределяем все функции для работы с критическими секциями.
// Определение класса CLock должно быть после этих строк
#define EnterCriticalSection EnterCriticalSectionDbg
#define TryEnterCriticalSection TryEnterCriticalSectionDbg
#define LeaveCriticalSection LeaveCriticalSectionDbg
#endif

Ну и заодно добавим еще один метод в наш класс CLock

Листинг 15. Класс CLock с новым методом

class CLock
{
    friend class CScopeLock;
    CRITICAL_SECTION m_CS;
public:
    void Init() { ::InitializeCriticalSection(&m_CS); }
    void Term() { ::DeleteCriticalSection(&m_CS); }

    void Lock() { ::EnterCriticalSection(&m_CS); }
    BOOL TryLock() { return ::TryEnterCriticalSection(&m_CS); }
    void Unlock() { ::LeaveCriticalSection(&m_CS); }
    BOOL Check() { return CheckCriticalSection(&m_CS); }
};

Использовать метод Check() в release-конфигурациях не стоит, возможно, что в будущем, в какой-нибудь Windows64, структура RTL_CRITICAL_SECTION изменится и результат такой проверки не определен. Так что ему самое место "жить" внутри всяческих ASSERT'ов.

Итак, что мы имеем? Мы имеем проверку на лишний вызов ::LeaveCriticalSection() и ту же трассировку для блокировок. Не так уж много. Особенно, если трассировка о блокировке имеет место, а вот нить, забывшая освободить критическую секцию, давно завершилась. Как быть? Вернее, что бы еще придумать, чтобы ошибку проще было выявить? Как минимум, прикрутить сюда __LINE__ и __FILE__, константы, соответствующие текущей строке и имени файла на момент компиляции этого метода.

VOID EnterCriticalSectionDbg(LPCRITICAL_SECTION pcs
    , int nLine = __LINE__, azFile = __FILE__);

Компилируем, запускаем... Результат удивительный. Хотя правильный. Компилятор честно подставил номер строки и имя файла, соответствующие началу нашей EnterCriticalSectionDbg(). Так что придется попотеть немного больше. __LINE__ и __FILE__ нужно вставить в #define'ы, тогда мы получим действительные номер строки и имя исходного файла. Теперь вопрос, куда же сохранить эти параметры для дальнейшего использования? Причем хочется оставить за собой возможность вызова стандартных функций API наряду с нашими собственными? На помощь приходит C++: просто создадим свою структуру, унаследовав ее от RTL_CRITICAL_SECTION. Итак:

Листинг 16. Реализация критических секций с сохранением строки и имени файла

#if defined(_DEBUG) && !defined(_NO_DEADLOCK_TRACE)

#define DEADLOCK_TIMEOUT 30000
#define CS_DEBUG 2

// Наша структура взамен CRITICAL_SECTION
struct CRITICAL_SECTION_DBG : public CRITICAL_SECTION
{
    // Добавочные поля
    int         m_nLine;
    LPCSTR        m_azFile;
};
typedef struct CRITICAL_SECTION_DBG *LPCRITICAL_SECTION_DBG;

// Создаем на лету событие для операций ожидания,
// но никогда его не освобождаем. Так удобней для отладки
static inline HANDLE _CriticalSectionGetEvent(LPCRITICAL_SECTION pcs)
{
    HANDLE ret = pcs->LockSemaphore;
    if (!ret)
    {
        HANDLE sem = ::CreateEvent(NULL, false, false, NULL);
        ATLASSERT(sem);

        if (!(ret = (HANDLE)::InterlockedCompareExchangePointer(
                &pcs->LockSemaphore, sem, NULL)))
            ret = sem;
        else
            ::CloseHandle(sem); // Кто-то успел раньше
    }
    return ret;
}

// Ждем, пока критическая секция не освободится либо время ожидания
// будет превышено
static inline VOID _WaitForCriticalSectionDbg(LPCRITICAL_SECTION_DBG pcs
    , int nLine, LPCSTR azFile)
{
    HANDLE sem = _CriticalSectionGetEvent(pcs);

    DWORD dwWait;
    do
    {
        dwWait = ::WaitForSingleObject(sem, DEADLOCK_TIMEOUT);
        if (WAIT_TIMEOUT == dwWait)
        {
            ATLTRACE("Critical section timeout (%u msec):"
                " tid 0x%04X owner tid 0x%04X\n"
                "Owner lock from %hs line %u, waiter %hs line %u\n"
                , DEADLOCK_TIMEOUT
                , ::GetCurrentThreadId(), pcs->OwningThread
                , pcs->m_azFile, pcs->m_nLine, azFile, nLine);
        }
    }while(WAIT_TIMEOUT == dwWait);
    ATLASSERT(WAIT_OBJECT_0 == dwWait);
}

// Выставляем событие в активное состояние
static inline VOID _UnWaitCriticalSectionDbg(LPCRITICAL_SECTION pcs)
{
    HANDLE sem = _CriticalSectionGetEvent(pcs);
    BOOL b = ::SetEvent(sem);
    ATLASSERT(b);
}

// Инициализируем критическую секцию.
inline VOID InitializeCriticalSectionDbg(LPCRITICAL_SECTION_DBG pcs)
{
    // Пусть система заполнит свои поля
    InitializeCriticalSection(pcs);
    // Заполняем наши поля
    pcs->m_nLine = 0;
    pcs->m_azFile = NULL;
}

// Освобождаем ресурсы, занимаемые критической секцией
inline VOID DeleteCriticalSectionDbg(LPCRITICAL_SECTION_DBG pcs)
{
    // Проверяем, чтобы не было удалений "захваченных" критических секций
    ATLASSERT(0 == pcs->m_nLine && NULL == pcs->m_azFile);
    // Остальное доделает система
    DeleteCriticalSection(pcs);
}

// Заполучем критическую секцию в свое пользование
inline VOID EnterCriticalSectionDbg(LPCRITICAL_SECTION_DBG pcs
    , int nLine, LPSTR azFile)
{
    if (::InterlockedIncrement(&pcs->LockCount))
    {
        // LockCount стал больше нуля.
        // Проверяем идентификатор нити
        if (pcs->OwningThread == (HANDLE)::GetCurrentThreadId())
        {
            // Нить та же самая. Критическая секция наша.
            // Никаких дополнительных действий не производим.
            // Это не совсем верно, так как возможно, что непарный
            // вызов ::LeaveCriticalSection() был на n-ном заходе,
            // и это прийдется отлавливать вручную, но реализация
            // стека для __LINE__ и __FILE__ сделает нашу систему
            // более громоздкой. Если это действительно необходимо,
            // Вы всегда можете сделать это самостоятельно
            pcs->RecursionCount++;
            return;
        }

        // Критическая секция занята другой нитью.
        // Придется подождать
        _WaitForCriticalSectionDbg(pcs, nLine, azFile);
    }

    // Либо критическая секция была "свободна",
    // либо мы дождались. Сохраняем идентификатор текущей нити.
    pcs->OwningThread = (HANDLE)::GetCurrentThreadId();
    pcs->RecursionCount = 1;
    pcs->m_nLine = nLine;
    pcs->m_azFile = azFile;
}

// Заполучаем критическую секцию если она никем не занята
inline BOOL TryEnterCriticalSectionDbg(LPCRITICAL_SECTION_DBG pcs
    , int nLine, LPSTR azFile)
{
    if (-1L == ::InterlockedCompareExchange(&pcs->LockCount, 0, -1))
    {
        // Это первое обращение к критической секции
        pcs->OwningThread = (HANDLE)::GetCurrentThreadId();
        pcs->RecursionCount = 1;
        pcs->m_nLine = nLine;
        pcs->m_azFile = azFile;
    }
    else if (pcs->OwningThread == (HANDLE)::GetCurrentThreadId())
    {
        // Это не первое обращение, но из той же нити
        ::InterlockedIncrement(&pcs->LockCount);
        pcs->RecursionCount++;
    }
    else
        return FALSE; // Критическая секция занята другой нитью

    return TRUE;
}

// Освобождаем критическую секцию
inline VOID LeaveCriticalSectionDbg(LPCRITICAL_SECTION_DBG pcs)
{
    // Проверяем, чтобы идентификатор текущей нити совпадал
    // с идентификатором нити-влядельца.
    // Если это не так, скорее всего мы имеем дело с ошибкой
    ATLASSERT(pcs->OwningThread == (HANDLE)::GetCurrentThreadId());

    if (--pcs->RecursionCount)
    {
        // Не последний вызов из этой нити.
        // Уменьшаем значение поля LockCount
        ::InterlockedDecrement(&pcs->LockCount);
    }
    else
    {
        // Последний вызов. Нужно "разбудить" какую-либо
        // из ожидающих ниток, если таковые имеются
        ATLASSERT(NULL != pcs->OwningThread);

        pcs->OwningThread = NULL;
        pcs->m_nLine = 0;
        pcs->m_azFile = NULL;
        if (::InterlockedDecrement(&pcs->LockCount) >= 0)
        {
            // Имеется, как минимум, одна ожидающая нить
            _UnWaitCriticalSectionDbg(pcs);
        }
    }
}

// Удостоверяемся, что ::EnterCriticalSection() была вызвана
// до вызова этого метода
inline BOOL CheckCriticalSection(LPCRITICAL_SECTION pcs)
{
    return pcs->LockCount >= 0
        && pcs->OwningThread == (HANDLE)::GetCurrentThreadId();
}

// Переопределяем все функции для работы с критическими секциями.
// Определение класса CLock должно быть после этих строк
#define InitializeCriticalSection InitializeCriticalSectionDbg
#define InitializeCriticalSectionAndSpinCount(pcs, c) \
         InitializeCriticalSectionDbg(pcs)
#define DeleteCriticalSection DeleteCriticalSectionDbg
#define EnterCriticalSection(pcs) EnterCriticalSectionDbg(pcs, __LINE__, __FILE__)
#define TryEnterCriticalSection(pcs) TryEnterCriticalSectionDbg(pcs, __LINE__, __FILE__)
#define LeaveCriticalSection LeaveCriticalSectionDbg
#define CRITICAL_SECTION CRITICAL_SECTION_DBG
#define LPCRITICAL_SECTION LPCRITICAL_SECTION_DBG
#define PCRITICAL_SECTION PCRITICAL_SECTION_DBG

#endif

Приводим наши классы в соответствие

Листинг 17. Классы CLock и CScopeLock, вариант для отладки

class CLock
{
    friend class CScopeLock;
    CRITICAL_SECTION m_CS;
public:
    void Init() { ::InitializeCriticalSection(&m_CS); }
    void Term() { ::DeleteCriticalSection(&m_CS); }

#if defined(CS_DEBUG)
    BOOL Check() { return CheckCriticalSection(&m_CS); }
#endif
#if CS_DEBUG > 1
    void Lock(int nLine, LPSTR azFile)
      { EnterCriticalSectionDbg(&m_CS, nLine, azFile); }
    BOOL TryLock(int nLine, LPSTR azFile)
      { return TryEnterCriticalSectionDbg(&m_CS, nLine, azFile); }
#else
    void Lock()
      { ::EnterCriticalSection(&m_CS); }
    BOOL TryLock()
      { return ::TryEnterCriticalSection(&m_CS); }
#endif
    void Unlock()
       { ::LeaveCriticalSection(&m_CS); }
};
class CScopeLock
{
    LPCRITICAL_SECTION m_pCS;
public:
#if CS_DEBUG > 1
    CScopeLock(LPCRITICAL_SECTION pCS, int nLine,
    LPSTR azFile) : m_pCS(pCS) { Lock(nLine, azFile); }
    CScopeLock(CLock& lock, int nLine,
    LPSTR azFile) : m_pCS(&lock.m_CS) { Lock(nLine, azFile); }
    void Lock(int nLine,
     LPSTR azFile) { EnterCriticalSectionDbg(m_pCS, nLine, azFile); }
#else
    CScopeLock(LPCRITICAL_SECTION pCS) : m_pCS(pCS) { Lock(); }
    CScopeLock(CLock& lock) : m_pCS(&lock.m_CS) { Lock(); }
    void Lock() { ::EnterCriticalSection(m_pCS); }
#endif
    ~CScopeLock() { Unlock(); }
    void Unlock() { ::LeaveCriticalSection(m_pCS); }
};

#if CS_DEBUG > 1
#define Lock() Lock(__LINE__, __FILE__)
#define TryLock() TryLock(__LINE__, __FILE__)
#define lock(cs) lock(cs, __LINE__, __FILE__)
#endif

К сожалению, пришлось даже переопределить CScopeLock lock(cs), причем мы жестко привязались к имени переменной. Не говоря уж о том, что у нас наверняка получился конфликт имен, все-таки Lock довольно популярное название для метода. Такой код не будет собираться, например, с популярнейшей библиотекой ATL. Тут есть два способа. Переименовать наши методы Lock() и TryLock() во что-нибудь более уникальное либо переименовать Lock() в ATL:

// StdAfx.h
// ...
#define Lock ATLLock
#include <AtlBase.h>
// ...

Сменим тему

А что это мы все про Win32 API да про C++? Давайте посмотрим, как обстоят дела с критическими секциями в более современных языках программирования.

C#

Тут мы стараниями Майкрософт имеем полный набор старого доброго API под новыми именами.

Критические секции представлены классом System.Threading.Monitor, вместо ::EnterCriticalSection() есть Monitor.Enter(object), а вместо ::LeaveCriticalSection() Monitor.Exit(object), где object - это любой объект C#. Т.е. каждый объект где-то в потрохах CLR (Common Language Runtime) имеет свою собственную критическую секцию. Либо заводит ее по необходимости. Типичное использование этой секции выглядит так:

Monitor.Enter(this);
m_dwSmth = dwSmth;
Monitor.Exit(this);

Если нужно организовать отдельную критическую секцию для какой-либо переменной самым логичным способом будет поместить ее в отдельный объект и использовать этот объект как аргумент при вызове Monitor.Enter/Exit(). Кроме того, в C# существует ключевое слово lock, это полный аналог нашего класса CScopeLock.

lock(this)
{
    m_dwSmth = dwSmth;
}

А вот Monitor.TryEnter() в C# (о, чюдо!) принимает в качестве параметра максимальный период ожидания.

Замечу, что CLR это не только C#, все это применимо и к другим языкам, использующим CLR.

Java

В этом языке используется подобный механизм, только место ключевого слова lock есть ключевое слово synchronized, а все остальное будет точно так же.

synchronized(this)
{
    m_dwSmth = dwSmth;
}

MC++ (управляемый C++)

Тут тоже появился атрибут [synchronized] ведущий себя точно также, как и одноименное ключевое слово из Java. Странно, что архитекторы из Майкрософт решили позаимствовать синтаксис из продукта от Sun Microsystems вместо своего собственного.

[synchronized] DWORD m_dwSmth;
//...
m_dwSmth = dwSmth; // неявный вызов Lock(this)

Delphi

Практически все, что верно для C++, верно и для Delphi. Критические секции представлены объектом TCriticalSection. Собственно, это такая же обертка как и наш класс CLock.

Кроме того, в Delphi присутствует специальный объект TMultiReadExclusiveWriteSynchronizer с названием, говорящим само за себя.

Подведем итоги

Итак, что нужно знать о критических секциях:

  • Критические секции работают быстро и не требуют большого количества системных ресурсов.
  • Для синхронизации доступа к нескольким (независимым) переменным лучше использовать несколько критических секций, а не одну для всех.
  • Код, ограниченный критическими секциями, лучше всего свести к минимуму.
  • Находясь в критической секции, не стоит вызовать методы "чужих" объектов.

Это все на сегодня. Пока!

Алекс Jenter jenter@rsdn.ru
Duisburg, 2001. Публикуемые в рассылке материалы принадлежат сайту RSDN.


ПРОГРАММИРОВАНИЕ НА V I S U A L C + +
РАССЫЛКА САЙТА
RSDN.RU
Выпуск No. 67 от 10 марта 2002 г.

РАССЫЛКА ЯВЛЯЕТСЯ ЧАСТЬЮ ПРОЕКТА RSDN , НА САЙТЕ КОТОРОГО ВСЕГДА МОЖНО НАЙТИ ВСЮ НЕОБХОДИМУЮ РАЗРАБОТЧИКУ ИНФОРМАЦИЮ, СТАТЬИ, ФОРУМЫ, РЕСУРСЫ, ПОЛНЫЙ АРХИВ ПРЕДЫДУЩИХ ВЫПУСКОВ РАССЫЛКИ И МНОГОЕ ДРУГОЕ.

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

Сегодня я хочу предложить вашему вниманию тему, которая еще ни разу не появлялась на страницах рассылки, хотя без сомнения этого заслуживает. Эта тема - графическая библиотека OpenGl, которая уже долгое время является фактическим стандартом для серьезных 3D приложений.

К сожалению из-за большого объема статьи ее пришлось разбить на две части. Но вторую часть вы получите сразу же в выпуске 67б, так что вам не придется ждать целую неделю ;-)

CТАТЬЯ

Учебное пособие по OpenGL
Авторы: Фролов Антон
Игнатенко Алексей
Источник: Лаборатория компьютерной графики при ВМиК МГУ
Введение

OpenGL является на данный момент одним из самых популярных программных интерфейсов (API) для разработки приложений в области двумерной и трехмерной графики. Стандарт OpenGL был разработан и утвержден в 1992 году ведущими фирмами в области разработки программного обеспечения, а его основой стала библиотека IRIS GL, разработанная Silicon Graphics.

На данный момент реализация OpenGL включает в себя несколько библиотек (описание базовых функций OpenGL, GLU,GLUT,GLAUX и другие), назначение которых будет описано ниже.

Что такое OpenGL?

С точки зрения программиста, OpenGL - это набор команд, которые описывают геометрические объекты и способ их отображения на экране. В большинстве случаев OpenGL предоставляет непосредственный интерфейс, т.е. определение объекта вызывает его визуализацию в буфере кадра.

Типичная программа, использующая OpenGL, начинается с определения окна, в котором будет происходить отображение. Затем создается контекст OpenGL и ассоциируется с этим окном. Далее программист может свободно использовать команды и операции OpenGL API. Часть команд используются для рисования простых геометрических объектов (т.е. точек, линий, многоугольников), тогда как другие задают режимы отображения этих примитивов. Например, можно задать режимы заливки цветом, отображение из трехмерной системы координат в экранную систему. Есть возможности для прямого контроля над буфером кадра, такие как чтение и запись пикселей.

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

OpenGL является прослойкой между аппаратурой и пользовательским уровнем. Это дает возможность использовать единый интерфейс для разных платформ, при этом получая оптимальную производительность с использованием аппаратной поддержки.

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

Характерными особенностями OpenGL, которые обеспечили распространение и развитие этого графического стандарта, являются:

Стабильность

Дополнения и изменения в стандарте реализуются таким образом, чтобы сохранить совместимость с разработанным ранее программным обеспечением.

Надежность и переносимость

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

Легкость применения

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

Основные возможности
  • Набор базовых примитивов: точки, линии, многоугольники и т.п.
  • Видовые и координатные преобразования
  • Удаление невидимых линий и поверхностей (z-буфер)
  • Использование сплайнов для построения линий и поверхностей
  • Наложение текстуры и применение освещения
  • Добавление специальных эффектов: тумана, изменение прозрачности, смешивание цветов (blending), устранение ступенчатости (anti-aliasing).

Как уже было сказано, существует реализация OpenGL для разных платформ, для чего было удобно разделить базовые функции графической системы и функции для отображения графической информации и взаимодействия с пользователем. Были созданы библиотеки для отображения информации с помощью оконной подсистемы для операционных систем Windows и Unix (WGL и GLX соответственно), а также библиотеки GLAUX и GLUT, которые используются для создания так называемых консольных приложений.

Библиотека GLAUX уступает по популярности написанной несколько позже библиотеке GLUT, хотя они предоставляют примерно одинаковые возможности. В дальнейшем в данном пособии в качестве основной будет рассматриваться библиотека GLUT, предоставляющая широкий набор средств взаимодействия с пользователем.

В состав библиотеки GLU вошла реализация более сложных функций, таких как набор популярных геометрических примитивов (куб, шар, цилиндр, диск), функции построения сплайнов, реализация дополнительных операций над матрицами и т.п. Все они реализованы через базовые функции OpenGL.

Основы OpenGL

С точки зрения архитектуры графическая система OpenGL является конвейером, состоящим из нескольких этапов обработки данных:

  • Аппроксимация кривых и поверхностей
  • Обработка вершин и сборка примитивов
  • Растеризация и обработка фрагментов
  • Операции над пикселями
  • Подготовка текстуры
  • Передача данных в буфер кадра

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

Примитивы определяются набором из одной или более вершин (vertex). Вершина определяет точку, конец грани, угол многоугольника. С каждой вершиной ассоциируются некоторые данные (координаты, цвет, нормаль, текстурные координаты). В подавляющем большинстве случаев каждая вершина обрабатывается независимо от других.

Команды OpenGL всегда обрабатываются в том порядке, в котором они поступают, хотя могут происходить задержки перед тем, как проявится эффект от их выполнения.

Синтаксис команд

Для обеспечения интуитивно понятных названий в OpenGL полное имя команды имеет вид:

type glCommand_name[1 2 3 4][b s i f d ub us ui][v](type1 arg1,:,typeN argN)

Таким образом, имя состоит из нескольких частей:

Gl | это имя библиотеки, в которой описана эта функция: для базовых функций OpenGL, функций из библиотек GLU, GLUT, GLAUX это gl, glu, glut, glaux соответственно
Command_name | имя команды
[1 2 3 4] | число аргументов команды
[b s i f d ub us ui] | тип аргумента: символ b означает тип GLbyte (аналог char в С\С++), символ f - тип GLfloat (аналог float), символ i - тип GLint (аналог int) и так далее. Полный список типов и их описание можно посмотреть в файле gl.h
[v] | наличие этого символа показывает, что в качестве параметров функции используется указатель на массив значений

Символы в квадратных скобках в некоторых названиях не используются. Например, команда glVertex2i() описана как базовая в библиотеке OpenGL, и использует в качестве параметров два целых числа, а команда glColor3fv() использует в качестве параметра указатель на массив из трех вещественных чисел.

Структура консольного приложения

Будем рассматривать построение консольного приложения при помощи библиотеки GLUT или GL Utility Toolkit, получившей в последнее время широкое распространение. Эта библиотека обеспечивает единый интерфейс для работы с окнами вне зависимости от платформы, поэтому описываемая ниже структура приложения остается неизменной для операционных систем Windows, Linux и многих других.

Функции GLUT могут быть классифицированы на несколько групп по своему назначению:

  • Инициализация
  • Начало обработки событий
  • Управление окнами
  • Управление меню
  • Регистрация вызываемых (callback) функций
  • Управление индексированной палитрой цветов
  • Отображение шрифтов
  • Отображение дополнительных геометрических фигур (тор, конус и др.)

Инициализация проводится с помощью функции

glutInit(int *argcp, char **argv)

Переменная argcp есть указатель на стандартную переменную argc описываемую в функции main(), а argv - указатель на параметры, передаваемые программе при запуске, который описывается там же. Эта функция проводит необходимые начальные действия для построения окна приложения, и только несколько функций GLUT могут быть вызваны до нее. К ним относятся:

glutInitWindowPosition(int x, int y)
glutInitWindowSize(int width, int height)
glutInitDisplayMode(unsigned int mode)

Первые две функции задают соответственно положение и размер окна, а последняя функция определяет различные режимы отображения информации, которые могут совместно задаваться с использованием операции побитового "или" ( " | " ) :

GLUT_RGBA | Режим RGBA. Используется по умолчанию, если не указаны явно режимы GLUT_RGBA или GLUT_INDEX.
GLUT_RGB | То же, что и GLUT_RGBA.
GLUT_INDEX | Режим индексированных цветов (использование палитры). Отменяет GLUT_RGBA.
GLUT_SINGLE | Окно с одиночным буфером. Используется по умолчанию.
GLUT_DOUBLE | Окно с двойным буфером. Отменяет GLUT_SINGLE.
GLUT_STENCIL | Окно с трафаретным буфером.
GLUT_ACCUM | Окно с буфером накопления.
GLUT_DEPTH | Окно с буфером глубины.

Это неполный список параметров для данной функции, однако, для большинства случаев этого бывает достаточно.

Двойной буфер обычно используют для анимации, сначала рисуя что-нибудь в одном буфере, а затем, меняя их местами, что позволяет избежать мерцания. Буфер глубины или z-буфер используется для удаления невидимых линий и поверхностей.

Работа с трафаретным буфером и буфером накопления описана в разделе Спецэффекты.

Функции библиотеки GLUT реализуют так называемый событийно-управляемый механизм. Это означает, что есть некоторый внутренний цикл, который запускается после соответствующей инициализации и обрабатывает, один за другим, все события, объявленные во время инициализации. К событиям относятся: щелчок мыши, закрытие окна, изменение свойств окна, передвижение курсора, нажатие клавиши, и "пустое" (idle) событие, когда ничего не происходит. Для проведения периодической проверки совершения того или иного события надо зарегистрировать функцию, которая будет его обрабатывать. Для этого используются функции вида:

void glutDisplayFunc(void (*func)(void))
void glutReshapeFunc(void (*func)(int width, int height))
void glutMouseFunc(void (*func)(int button, int state, int x, int y))
void glutIdleFunc(void (*func)(void))

То есть параметром для них является имя соответствующей функции заданного типа. С помощью glutDisplayFunc() задается функция рисования для окна приложения, которая вызывается при необходимости создания или восстановления изображения. Для явного указания, что окно надо обновить, иногда удобно использовать функцию

void glutPostRedisplay(void)

Через glutReshapeFunc() устанавливается функция обработки изменения размеров окна пользователем, которой передаются новые размеры.

glutMouseFunc() определяет обработчика команд от мыши, а glutIdleFunc() задает функцию, которая будет вызываться каждый раз, когда нет событий от пользователя.

Контроль всех событий происходит внутри бесконечного цикла в функции

void glutMainLoop(void)

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

#include <GL/glut.h>
void MyIdle(void)
{
  /*Код, который меняет переменные, определяющие следующий  кадр */
  ...
}
void MyDisplay(void)
{
  /* Код OpenGL, который отображает кадр */
  ...
  /* После рисования переставляем  буфера */
  glutSwapBuffers();
}
void main(int argcp, char **argv)
{
  /* Инициализация GLUT */
  glutInit(&argcp, argv);
  glutInitWindowSize(640, 480);
  glutInitWindowPosition(0, 0);
  /* Открытие окна */
  glutCreateWindow("My OpenGL Application");
  /* Выбор режима:  двойной буфер и RGBA цвета */
  glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH);
  /* Регистрация вызываемых функций */
  glutDisplayFunc(MyDisplay);
  glutIdleFunc(MyIdle);
  /* Запуск механизма обработки событий */
  glutMainLoop();
}

В случае если приложение должно строить статичное изображение, можно заменить GLUT_DOUBLE на GLUT_SINGLE, так как одного буфера в этом случае будет достаточно, и убрать вызов функции glutIdleFunc().

Вершины и примитивы
Положение вершины в пространстве

Положение вершины определяются заданием их координат в двух-, трех-, или четырехмерном пространстве (однородные координаты). Это реализуется с помощью нескольких версий команды glVertex:

void glVertex[2 3 4][s i f d](type coords)
void glVertex[2 3 4][s i f d]v(type *coords)

Каждая команда задает 4 координаты вершины: x, y, z и w. Команда glVertex2 получает значения x и y. Координата z в таком случае устанавливается по умолчанию равной 0, а координата w равной 1. Vertex3 получает координаты x, y, z и заносит в координату w значение 1. Vertex4 позволяет задать все 4 координаты.

Для ассоциации с вершинами цветов, нормалей, и текстурных координат используются текущие значения соответствующих данных. Эти значения могут быть изменены в любой момент путем использования соответствующих команд.

Цвет вершины

Для задания текущего цвета вершины используются команды

void glColor[3 4][b s i f](GLtype components)
void glColor[3 4][b s i f]v(GLtype components)

Первые три параметра задают R, G, B компоненты цвета, а последний параметр определяет alpha-компоненту, которая задает уровень прозрачности объекта. Если в названии команды указан тип 'f' (float), то значения всех параметров должны принадлежать отрезку [0,1], при этом по умолчанию значение alpha-компоненты устанавливается равным 1.0, что соответствует полной непрозрачности. Если указан тип 'ub' (unsigned byte), то значения должны лежать в отрезке [0,255].

Разным вершинам можно назначать различные цвета, и тогда будет проводиться линейная интерполяция цветов по поверхности примитива.

Для управления режимом интерполяции цветов используется команда

void glShadeModel(GLenum  mode)

вызов которой с параметром GL_SMOOTH включает интерполяцию (установка по умолчанию), а с GL_FLAT - отключает.

Нормаль

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

void glNormal3[b s i f d](type coords)
void glNormal3[b s i f d]v(type coords)

Задаваемый вектор может не иметь единичной длины, но он будет нормироваться автоматически в режиме нормализации, который включается вызовом команды glEnable(GL_NORMALIZE). Команды

void glEnable(GLenum mode)
void glDisable(GLenum mode)

производят включение и отключение того или иного режима работы конвейера OpenGL. Эти команды применяются достаточно часто, и их влияние будет рассматриваться в конкретных случаях.

ПРИМЕЧАНИЕ

Включение режима GL_NORMALIZE негативно влияет на скорость работы механизма визуализации OpenGL. В силу этого более предпочтительным является задание заранее приведенных к единичной длине нормалей. Нормализация нормалей необходима для правильного расчета освещения. Режим автоматической нормализации должен быть включен, если приложением используется преобразования растяжения/сжатия, поскольку в этом случае длина нормалей изменяется при умножении на видовую матрицу.
Операторные скобки Begin/End

Мы рассмотрели задание атрибутов одной вершины. Однако чтобы задать какую-нибудь фигуру, одних координат вершин недостаточно, и эти вершины надо объединить в одно целое, определив необходимые свойства. Для этого в OpenGL используется понятие примитивов, к которым относятся точки, линии, связанные или замкнутые линии, треугольники и так далее. Задание примитива происходит внутри командных скобок:

void glBegin(GLenum mode);
void glEnd(void);

Параметр mode определяет тип примитива, который задается внутри и может принимать следующие значения:

GL_POINTS | каждая вершина задает координаты некоторой точки.
GL_LINES | каждая отдельная пара вершин определяет отрезок; если задано нечетное число вершин, то последняя вершина игнорируется.
GL_LINE_STRIP | каждая следующая вершина задает отрезок вместе с предыдущей.
GL_LINE_LOOP | отличие от предыдущего примитива только в том, что последний отрезок определяется последней и первой вершиной, образуя замкнутую ломаную.
GL_TRIANGLES | каждая отдельная тройка вершин определяет треугольник; если задано не кратное трем число вершин, то последние вершины игнорируются.
GL_TRIANGLE_STRIP | каждая следующая вершина задает треугольник вместе с двумя предыдущими.
GL_TRIANGLE_FAN | треугольники задаются первой и каждой следующей парой вершин (пары не пересекаются).
GL_QUADS | каждая отдельная четверка вершин определяет четырехугольник; если задано не кратное четырем число вершин, то последние вершины игнорируются.
GL_QUAD_STRIP | четырехугольник с номером n определяется вершинами с номерами 2n-1, 2n, 2n+2, 2n+1.
GL_POLYGON | последовательно задаются вершины выпуклого многоугольника.

Рис. 1

ПРИМЕЧАНИЕ

Использование GL_TRIANGLE_STRIP и GL_TRIANGLE_FAN позволяет повысить производительность приложения.

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

GLfloat BlueCol[3] = {0,0,1};
glBegin(GL_TRIANGLE);
glColor3f(1.0, 0.0, 0.0); /* красный */
glVertex3f(0.0, 0.0, 0.0);
glColor3ub(0, 255, 0); /* зеленый */
glVertex3f(1.0, 0.0, 0.0);
glColor3fv(BlueCol); /* синий */
glVertex3f(1.0, 1.0, 0.0);
glEnd();

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

Однако сначала надо определить понятие лицевых и обратных граней.

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

void glFrontFace(GLenum mode)

со значением параметра mode равным GL_CW, а отменить - с GL_CCW.

Чтобы изменить метод отображения многоугольника используется команда

void glPolygonMode(GLenum face, Glenum mode)

Параметр mode определяет, как будут отображаться многоугольники, а параметр face устанавливает тип многоугольников, к которым будет применяться эта команда и может принимать следующие значения:

GL_FRONT | для лицевых граней
GL_BACK | для обратных граней
GL_FRONT_AND_BACK | для всех граней

Параметр mode может быть равен:

GL_POINT | при таком режиме будут отображаться только вершины многоугольников.
GL_LINE | при таком режиме многоугольник будет представляться набором отрезков.
GL_FILL | при таком режиме многоугольники будут закрашиваться текущим цветом с учетом освещения, и этот режим установлен по умолчанию.

Кроме того, можно указывать, какой тип граней отображать на экране. Для этого сначала надо установить соответствующий режим вызовом команды glEnable(GL_CULL_FACE), а затем выбрать тип отображаемых граней с помощью команды

void glCullFace(GLenum mode)

Вызов с параметром GL_FRONT приводит к удалению из изображения всех лицевых граней, а с параметром GL_BACK - обратных (установка по умолчанию).

Кроме рассмотренных стандартных примитивов в библиотеках GLU и GLUT описаны более сложные фигуры, такие как сфера, цилиндр, диск (в GLU) и сфера, куб, конус, тор, тетраэдр, додекаэдр, икосаэдр, октаэдр и чайник (в GLUT). Автоматическое наложение текстуры предусмотрено только для фигур из библиотеки GLU (создание текстур в OpenGL будет рассматриваться ниже).

Например, чтобы нарисовать сферу или цилиндр, надо сначала создать объект специального типа GLUquadricObj с помощью команды

GLUquadricObj* gluNewQuadric(void);

а затем вызвать соответствующую команду:

void gluSphere(GLUquadricObj * qobj, GLdouble radius, GLint slices, GLint stacks)
void gluCylinder(GLUquadricObj * qobj,GLdouble baseRadius,GLdouble topRadius,
     GLdouble height, GLint slices, GLint stacks)

где параметр slices задает число разбиений вокруг оси z, а stacks - вдоль оси z.

Более подробную информацию об этих и других командах построения примитивов можно найти в приложении.

ПРИМЕЧАНИЕ

Для корректного построения перечисленных примитивов необходимо удалять невидимые линии и поверхности, для чего надо включить соответствующий режим вызовом команды glEnable(GL_DEPTH_TEST).
Массивы вершин

Если вершин много, то чтобы не вызывать для каждой команду glVertex..(), удобно объединять вершины в массивы, используя команду

void glVertexPointer(GLint size, GLenum type, GLsizei stride, void *ptr)

которая определяет способ хранения и координаты вершин. При этом size определяет число координат вершины (может быть равен 2, 3, 4), type определяет тип данных (может быть равен GL_SHORT, GL_INT, GL_FLOAT, GL_DOUBLE). Иногда удобно хранить в одном массиве другие атрибуты вершины, и тогда параметр stride задает смещение от координат одной вершины до координат следующей; если stride равен нулю, это значит, что координаты расположены последовательно. В параметре ptr указывается адрес, где находятся данные.

Аналогично можно определить массив нормалей, цветов и некоторых других атрибутов вершины, используя команды

void NormalPointer(GLenum type, GLsizei stride, void *pointer )
void ColorPointer(GLint size, GLenum type, GLsizei stride, void *pointer )

Для того чтобы эти массивы можно было использовать в дальнейшем, надо вызвать команду

void  glEnableClientState(GLenum array)

с параметрами GL_VERTEX_ARRAY, GL_NORMAL_ARRAY, GL_COLOR_ARRAY соответственно. После окончания работы с массивом желательно вызвать команду

void glDisableClientState(GLenum array)

с соответствующим значением параметра array.

Для отображения содержимого массивов используется команда

void glArrayElement(GLint index)

которая передает OpenGL атрибуты вершины, используя элементы массива с номером index. Это аналогично последовательному применению команд вида glColor..(:), glNormal..(:), glVertex..(:) c соответствующими параметрами. Однако вместо нее обычно вызывается команда

void glDrawArrays(GLenum mode, GLint first, GLsizei count)

рисующая count примитивов, определяемых параметром mode, используя элементы из массивов с индексами от first до first+count-1. Это эквивалентно вызову команды glArrayElement() с соответствующими индексами.

В случае если одна вершина входит в несколько примитивов, то вместо дублирования ее координат в массиве удобно использовать ее индекс.

Для этого надо вызвать команду

void glDrawArrays(GLenum mode, GLsizei count, GLenum type, void *indices)

где indices - это массив номеров вершин, которые надо использовать для построения примитивов, type определяет тип элементов этого массива: GL_UNSIGNED_BYTE, GL_UNSIGNED_SHORT, GL_UNSIGNED_INT, а count задает их количество.

ПРИМЕЧАНИЕ

Использование массивов вершин позволяет повысить скорость визуализации трехмерной сцены
Списки отображения

Если нужно несколько раз обращаться к одной и той же группе команд, эти команды можно объединить в так называемый список изображений (display list) и вызывать его при необходимости. Для того чтобы создать новый список изображений надо поместить все команды, которые должны в него войти между командными скобками:

void glNewList(GLuint list, GLenum mode)
void glEndList()

Для различения списков используются целые положительные числа, задаваемые при создании списка значением параметра list, а параметр mode определяет режим обработки команд, входящих в список:

GL_COMPILE | команды записываются в список без выполнения
GL_COMPILE_AND_EXECUTE | команды сначала выполняются, а затем записываются в список

После того, как список создан, его можно вызвать командой

void glCallList(GLuint list)

указав в параметре list идентификатор нужного списка. Чтобы вызвать сразу несколько списков, можно воспользоваться командой

void glCallLists(GLsizei n, GLenum type, const GLvoid *lists)

вызывающей n списков с идентификаторами из массива lists, тип элементов которого указывается в параметре type. Это могут быть типы GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT, GL_INT, GL_UNSIGNED_INT и некоторые другие. Для удаления списков используется команда

void glDeleteLists(GLint list, GLsizei range)

которая удаляет списки с идентификаторами ID из диапазона list <= ID <= list+range-1.

Преобразования координат

В OpenGL используются как основные три системы координат: левосторонняя, правосторонняя и оконная. Первые две системы являются трехмерными и отличаются друг от друга направлением оси z: в правосторонней она направлена на наблюдателя, а в левосторонней - в глубь экрана. Расположение осей x и y аналогично описанному выше. Левосторонняя система используется для задания значений параметрам команды gluPerspective(), glOrtho(), которые будут рассмотрены ниже, а правосторонняя или мировая система координат во всех остальных случаях. Отображение трехмерной информации происходит в двумерную оконную систему координат.

Работа с матрицами

Для задания различных преобразований объектов сцены в OpenGL используются операции над матрицами, при этом различают три типа матриц: видовая, проекций и текстуры. Все они имеют размер 4x4. Видовая матрица определяет преобразования объекта в мировых координатах, такие как параллельный перенос, изменение масштаба и поворот. Матрица проекций задает, как будут проецироваться трехмерные объекты на плоскость экрана (в оконные координаты), а матрица текстуры определяет наложение текстуры на объект.

Для того чтобы выбрать, какую матрицу надо изменить, используется команда

void glMatrixMode(GLenum mode)

вызов которой со значением параметра mode равным GL_MODELVIEW, GL_PROJECTION, GL_TEXTURE включает режим работы с видовой, проекций и матрицей текстуры соответственно. Для вызова команд, задающих матрицы того или иного типа необходимо сначала установить соответствующий режим.

Для определения элементов матрицы текущего типа вызывается команда

void glLoadMatrix[f d](GLtype *m)

где m указывает на массив из 16 элементов типа float или double в соответствии с названием команды, при этом сначала в нем должен быть записан первый столбец матрицы, затем второй, третий и четвертый.

Команда

void glLoadIdentity(void)

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

void glPushMatrix(void)
void glPopMatrix(void)

Они записывают и восстанавливают текущую матрицу из стека, причем для каждого типа матриц стек свой. Для видовых матриц его глубина равна как минимум 32, а для двух оставшихся типов как минимум 2.

Для умножения текущей матрицы слева на другую матрицу используется команда

void glMultMatrix[f d](GLtype *m)

где m должен задавать матрицу размером 4x4 в виде массива с описанным расположением данных. Однако обычно для изменения матрицы того или иного типа удобно использовать специальные команды, которые по значениям своих параметров создают нужную матрицу и перемножают ее с текущей. Чтобы сделать текущей созданную матрицу, надо перед вызовом этой команды вызвать glLoadIdentity().

В целом, для отображения трехмерных объектов сцены в окно приложения используется следующая последовательность действий:

Рис. 2

Видовые преобразования

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

(x', y', z', 1) = M * (x, y, z, 1)

где M - матрица видового преобразования. Перспективное преобразование и проектирование производится аналогично. Сама матрица может быть создана с помощью следующих команд:

void glTranslate[f d](GLtype x, GLtype y, GLtype z)
void glRotate[f d](GLtype angle, GLtype x, GLtype y, GLtype z)
void glScale[f d](GLtype x, GLtype y, GLtype z)

glTranlsate..() производит перенос объекта, прибавляя к координатам его вершин значения своих параметров.

glRotate..() производит поворот объекта против часовой стрелки на угол angle (измеряется в градусах) вокруг вектора (x,y,z).

glScale..() производит масштабирование объекта (сжатие или растяжение), домножая соответствующие координаты его вершин на значения своих параметров.

Все эти преобразования будут применяться к примитивам, описания которых будут находиться ниже в программе. В случае если надо, например, повернуть один объект сцены, а другой оставить неподвижным, удобно сначала сохранить текущую видовую матрицу в стеке командой glPushMatrix(), затем вызвать glRotate..() с нужными параметрами, описать примитивы, из которых состоит этот объект, а затем восстановить текущую матрицу командой glPopMatrix().

Кроме изменения положения самого объекта иногда бывает нужно изменить положение точки наблюдения, что, однако также приводит к изменению видовой матрицы. Это можно сделать с помощью команды

void gluLookAt(GLdouble eyex, GLdouble eyey, GLdouble eyez,
         GLdouble centerx, GLdouble centery, GLdouble centerz,
            GLdouble upx, GLdouble upy, GLdouble upz)

где точка (eyex,eyey,eyez) определяет точку наблюдения, (centerx, centery, centerz) задает центр сцены, который будет проектироваться в центр области вывода, а вектор (upx,upy,upz) задает положительное направление оси у, определяя поворот камеры. Если, например, камеру не надо поворачивать, то задается значение (0,1,0), а со значением (0,-1,0) сцена будет перевернута.

Фактически, эта команда совершает перенос и поворот объектов сцены, но в таком виде задавать параметры бывает удобнее.

Проекции

В OpenGL существуют ортографическая (параллельная) и перспективная проекция. Первый тип проекции может быть задан командами

void glOrtho(GLdouble left, GLdouble right, GLdouble bottom, GLdouble top,
        GLdouble near, GLdouble far)
void gluOrtho2D(GLdouble left, GLdouble right, GLdouble bottom, GLdouble top)

Первая команда создает матрицу проекции в усеченный объем видимости (параллелограмм видимости) в левосторонней системе координат. Параметры команды задают точки (left, bottom, -near) и (right, top, -near), которые отвечают левому нижнему и правому верхнему углам окна вывода. Параметры near и far задают расстояние до ближней и дальней плоскостей отсечения по дальности от точки (0,0,0) и могут быть отрицательными.

Во второй команде, в отличие от первой, значения near и far устанавливаются равными -1 и 1 соответственно.

Перспективная проекция определяется командой

void gluPerspective(GLdouble angley, GLdouble aspect, GLdouble znear, GLdouble zfar)

которая задает усеченный конус видимости в левосторонней системе координат. Параметр angley определяет угол видимости в градусах по оси у и должен находиться в диапазоне от 0 до 180. Угол видимости вдоль оси x задается параметром aspect, который обычно задается как отношение сторон области вывода. Параметры zfar и znear задают расстояние от наблюдателя до плоскостей отсечения по глубине и должны быть положительными. Чем больше отношение zfar/znear, тем хуже в буфере глубины будут различаться расположенные рядом поверхности, так как по умолчанию в него будет записываться 'сжатая' глубина в диапазоне от 0 до 1 (см. следующий пункт).

Область вывода
После применения матрицы проекций на вход следующего преобразования подаются так называемые усеченные (clip) координаты, для которых значения всех компонент (x, y, z, w) находятся в отрезке [-1,1]. После этого находятся нормализованные координаты вершин по формуле: (x, y, z) = (x/w, y/w, z/w) Область вывода представляет собой прямоугольник в оконной системе координат, размеры которого задаются командой:

void glViewPort(GLint x, GLint y, GLint width, GLint height)

Значения всех параметров задаются в пикселях и определяют ширину и высоту области вывода с координатами левого нижнего угла (x,y) в оконной системе координат. Размеры оконной системы координат определяются текущими размерами окна приложения, точка (0,0) находится в левом нижнем углу окна.

Используя параметры команды glViewPort(), вычисляются оконные координаты центра области вывода (o,o) по формулам o=x+width/2, o=y+height/2. Пусть p=width, p=height, тогда можно найти оконные координаты каждой вершины: (x, y, z)= ( (p/2) x+ o, (p/2) y+ o[(f-n)/2] z+(n+f)/2 )При этом целые положительные величины n и f задают минимальную и максимальную глубину точки в окне и по умолчанию равны 0 и 1 соответственно. Глубина каждой точки записывается в специальный буфер глубины (z-буфер), который используется для удаления невидимых линий и поверхностей. Установить значения n и f можно вызовом функции

void glDepthRange(GLclampd n, GLclampd f)

Команда glViewPort() обычно используется в функции, зарегистрированной с помощью команды glutReshapeFunc(), которая вызывается, если пользователь изменяет размеры окна приложения, изменяя соответствующим образом область вывода.

Продолжение статьи - в выпуске 67б .

Алекс Jenter jenter@rsdn.ru
Duisburg, 2001. Публикуемые в рассылке материалы принадлежат сайту RSDN.


ПРОГРАММИРОВАНИЕ НА V I S U A L C + +
РАССЫЛКА САЙТА
RSDN.RU
Выпуск No. 67б от 10 марта 2002 г.

РАССЫЛКА ЯВЛЯЕТСЯ ЧАСТЬЮ ПРОЕКТА RSDN , НА САЙТЕ КОТОРОГО ВСЕГДА МОЖНО НАЙТИ ВСЮ НЕОБХОДИМУЮ РАЗРАБОТЧИКУ ИНФОРМАЦИЮ, СТАТЬИ, ФОРУМЫ, РЕСУРСЫ, ПОЛНЫЙ АРХИВ ПРЕДЫДУЩИХ ВЫПУСКОВ РАССЫЛКИ И МНОГОЕ ДРУГОЕ.

CТАТЬЯ

Учебное пособие по OpenGL
Продолжение. Начало см. выпуск 67
Авторы: Фролов Антон
Игнатенко Алексей
Источник: Лаборатория компьютерной графики при ВМиК МГУ
Материалы и освещение

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

Свойства материала

Для задания параметров текущего материала используются команды

void glMaterial[i f](GLenum face, GLenum pname, GLtype param)
void glMaterial[i f]v(GLenum face, GLenum pname, GLtype *params)

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

GL_AMBIENT | параметр params должен содержать четыре целых или вещественных значения цветов RGBA, которые определяют рассеянный цвет материала (цвет материала в тени). Значение по умолчанию: (0.2, 0.2, 0.2, 1.0).
GL_DIFFUSE | параметр params должен содержать четыре целых или вещественных значения цветов RGBA, которые определяют цвет диффузного отражения материала. Значение по умолчанию: (0.8, 0.8, 0.8, 1.0).
GL_SPECULAR | параметр params должен содержать четыре целых или вещественных значения цветов RGBA, которые определяют цвет зеркального отражения материала. Значение по умолчанию: (0.0, 0.0, 0.0, 1.0).
GL_SHININESS | параметр params должен содержать одно целое или вещественное значение в диапазоне от 0 до 128, которое определяет степень зеркального отражения материала. Значение по умолчанию: 0.
GL_EMISSION | параметр params должен содержать четыре целых или вещественных значения цветов RGBA, которые определяют интенсивность излучаемого света материала. Значение по умолчанию: (0.0, 0.0, 0.0, 1.0).
GL_AMBIENT_AND_DIFFUSE | эквивалентно двум вызовам команды glMaterial..() со значением pname GL_AMBIENT и GL_DIFFUSE и одинаковыми значениями params.

Из этого следует, что вызов команды glMaterial[i f]() возможен только для установки степени зеркального отражения материала. В большинстве моделей учитывается диффузный и зеркальный отраженный свет; первый определяет естественный цвет объекта, а второй - размер и форму бликов на его поверхности.

Параметр face определяет тип граней, для которых задается этот материал и может принимать значения GL_FRONT, GL_BACK или GL_FRONT_AND_BACK.

Если в сцене материалы объектов различаются лишь одним параметром, рекомендуется сначала установить нужный режим, вызвав glEnable() c параметром GL_COLOR_MATERIAL, а затем использовать команду

void glColorMaterial(GLenum face, GLenum pname)

где параметр face имеет аналогичный смысл, а параметр pname может принимать все перечисленные значения. После этого, значения выбранного с помощью pname свойства материала для конкретного объекта (или вершины) устанавливается вызовом команды glColor..(), что позволяет избежать вызовов более ресурсоемкой команды glMaterial..() и повышает эффективность программы.

Пример.

float mat_dif[]={0.8,0.8,0.8};
float mat_amb[]= {0.2,0.2,0.2};
float mat_spec[]={0.6,0.6,0.6};
float shininess=0.7*128;
/*...*/
glMaterialfv(GL_FRONT_AND_BACK,GL_AMBIENT,mat_amb);
glMaterialfv(GL_FRONT_AND_BACK,GL_DIFFUSE,mat_dif);
glMaterialfv(GL_FRONT_AND_BACK,GL_SPECULAR,mat_spec);
glMaterialf(GL_FRONT,GL_SHININESS,shininess);

Источники света

Добавить в сцену источник света можно с помощью команд

void glLight[i f](GLenum light, GLenum pname, GLfloat param)
void glLight[i f](GLenum light, GLenum pname, GLfloat *params)

Параметр light однозначно определяет источник, и выбирается из набора специальных символических имен вида GL_LIGHTi , где i должно лежать в диапазоне от 0 до GL_MAX_LIGHT, которое не превосходит восьми.

Оставшиеся два параметра имеют аналогичный смысл, что и в команде glMaterial..(). Рассмотрим их назначение (вначале описываются параметры для первой команды, затем для второй):

GL_SPOT_EXPONENT | параметр param должен содержать целое или вещественное число от 0 до 128, задающее распределение интенсивности света. Этот параметр описывает уровень сфокусированности источника света. Значение по умолчанию: 0 (рассеянный свет).
GL_SPOT_CUTOFF | параметр param должен содержать целое или вещественное число между 0 и 90 или равное 180, которое определяет максимальный угол разброса света. Значение этого параметра есть половина угла в вершине конусовидного светового потока, создаваемого источником. Значение по умолчанию: 180 (рассеянный свет).
GL_AMBIENT | параметр params должен содержать четыре целых или вещественных значения цветов RGBA, которые определяют цвет фонового освещения. Значение по умолчанию: (0.0, 0.0, 0.0, 1.0).
GL_DIFFUSE | параметр params должен содержать четыре целых или вещественных значения цветов RGBA, которые определяют цвет диффузного освещения. Значение по умолчанию: (1.0, 1.0, 1.0, 1.0) для LIGHT0 и (0.0, 0.0, 0.0, 1.0) для остальных.
GL_SPECULAR | параметр params должен содержать четыре целых или вещественных значения цветов RGBA, которые определяют цвет зеркального отражения. Значение по умолчанию: (1.0, 1.0, 1.0, 1.0) для LIGHT0 и (0.0, 0.0, 0.0, 1.0) для остальных.
GL_POSITION | параметр params должен содержать четыре целых или вещественных, которые определяют положение источника света. Если значение компоненты w равно 0.0, то источник считается бесконечно удаленным и при расчете освещенности учитывается только направление на точку (x,y,z), в противном случае считается, что источник расположен в точке (x,y,z,w). Значение по умолчанию: (0.0, 0.0, 1.0, 0.0).
GL_SPOT_DIRECTION | параметр params должен содержать четыре целых или вещественных числа, которые определяют направление света. Значение по умолчанию: (0.0, 0.0, -1.0, 1.0).

При изменении положения источника света следует учитывать следующие факты: если положение задается командой glLight..() перед определением ориентации взгляда (командой glLookAt()), то будет считаться, что источник находится в точке наблюдения. Если положение устанавливается между заданием ориентации и преобразованиями видовой матрицы, то оно фиксируется и не зависит от видовых преобразований. В последнем случае, когда положение задано после ориентации и видовой матрицы, его положение можно менять, устанавливая как новую ориентацию наблюдателя, так и меняя видовую матрицу.

Для использования освещения сначала надо установить соответствующий режим вызовом команды glEnable(GL_LIGHTNING), а затем включить нужный источник командой glEnable(GL_LIGHTn).

ПРИМЕЧАНИЕ

Еще раз обратим внимание на то, что при выключенном освещении цвет вершины равен текущему цвету, который задается командами glColor..(). При включенном освещении цвет вершины вычисляется исходя из информации о материале, о нормалях и об источниках света.

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

В OpenGL используется модель освещения, в соответствии с которой цвет точки определяется несколькими факторами: свойствами материала и текстуры, величиной нормали в этой точке, а также положением источника света и наблюдателя. Для корректного расчета освещенности в точке надо использовать единичные нормали, однако команды типа glScale..(), могут изменять длину нормалей. Чтобы это учитывать, используется уже упоминавшийся режим нормализации нормалей, который включается вызовом команды glEnable(GL_NORMALIZE).

Для задания глобальных параметров освещения используются команды

void glLightModel[i f](GLenum pname, GLenum param)
void glLightModel[i f]v(GLenum pname, const GLtype *params)

Аргумент pname определяет, какой параметр модели освещения будет настраиваться и может принимать следующие значения:

GL_LIGHT_MODEL_LOCAL_VIEWER | параметр param должен быть булевским и задает положение наблюдателя. Если он равен FALSE, то направление обзора считается параллельным оси -z, вне зависимости от положения в видовых координатах. Если же он равен TRUE, то наблюдатель находится в начале видовой системы координат. Это может улучшить качество освещения, но усложняет его расчет. Значение по умолчанию: FALSE.
GL_LIGHT_MODEL_TWO_SIDE | параметр param должен быть булевским и управляет режимом расчета освещенности как для лицевых, так и для обратных граней. Если он равен FALSE, то освещенность рассчитывается только для лицевых граней. Если же он равен TRUE, расчет проводится и для обратных граней. Значение по умолчанию: FALSE.
GL_LIGHT_MODEL_AMBIENT | параметр params должен содержать четыре целых или вещественных числа, которые определяют цвет фонового освещения даже в случае отсутствия определенных источников света. Значение по умолчанию: (0.2, 0.2, 0.2,1.0).
Текстуры

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

выбрать изображение и преобразовать его к нужному формату

загрузить изображение в память

определить, как текстура будет наноситься на объект и как она будет с ним взаимодействовать.

Подготовка текстуры

Для использования текстуры необходимо сначала загрузить в память изображение нужной текстуры и передать его OpenGL.

Считывание графических данных из файла и их преобразование можно проводить вручную. Можно также воспользоваться функцией, входящей в состав библиотеки GLAUX (для ее использования надо дополнительно подключить glaux.lib), которая сама проводит необходимые операции. Это функция

AUX_RGBImageRec* auxDIBImageLoad(const char *file)

где file - название файла с расширением *.bmp или *.dib. В качестве результата функция возвращает указатель на область памяти, где хранятся преобразованные данные.

Сейчас предположим, что изображение уже загружено и рассмотрим, каким образом его можно передать механизмам OpenGL.

При создании образа текстуры в памяти следует учитывать следующие требования.

Во-первых, размеры текстуры, как по горизонтали, так и по вертикали должны представлять собой степени двойки. Это требование накладывается для компактного размещения текстуры в памяти и способствует ее эффективному использованию. Использовать только текстуры с такими размерами конечно неудобно, поэтому после загрузки их надо преобразовать. Изменение размеров текстуры можно провести с помощью команды

void gluScaleImage(GLenum format, GLint widthin, GL heightin, GLenum typein,
        const void *datain, GLint widthout, GLint heightout, GLenum typeout,
        void *dataout)

В качестве значения параметра format обычно используется значение GL_RGB или GL_RGBA, определяющее формат хранения информации. Параметры widthin, heightin, widhtout, heightout определяют размеры входного и выходного изображений, а с помощью typein и typeout задается тип элементов массивов, расположенных по адресам datain и dataout. Как и обычно, то может быть тип GL_UNSIGNED_BYTE, GL_SHORT, GL_INT и так далее. Результат своей работы функция заносит в область памяти, на которую указывает параметр dataout.

Во-вторых, надо предусмотреть случай, когда объект по размерам значительно меньше наносимой на него текстуры. Чем меньше объект, тем меньше должна быть наносимая на него текстура и поэтому вводится понятие уровней детализации текстуры. Каждый уровень детализации задает некоторое изображение, которое является, как правило, уменьшенной в два раза копией оригинала. Такой подход позволяет улучшить качество нанесения текстуры на объект. Например, для изображения размером 2x2можно построить max(m,n)+1 уменьшенных изображений, соответствующих различным уровням детализации.

Эти два этапа создания образа текстуры во внутренней памяти OpenGL можно провести с помощью команды

void gluBuild2DMipmaps(GLenum target, GLint components, GLint width,
      GLint height, GLenum format, GLenum type, const void *data)

где параметр target должен быть равен GL_TEXTURE_2D, components определяет количество цветовых компонент текстуры и может принимать следующие значения:

GL_LUMINANCE | только красный. (текстура будет монохромной)
GL_LUMINANCE_ALPHA | красный и alpha.
GL_RGB | красный, синий, зеленый
GL_RGBA | все компоненты.

Параметры width, height, data определяют размеры и расположение текстуры соответственно, а format и type имеют аналогичный смысл, что и в команде gluScaleImage().

После выполнения этой команды текстура копируется во внутреннюю память OpenGL и поэтому память, занимаемую исходным изображением текстуры можно освободить.

В OpenGL допускается использование одномерных текстур, то есть размера 1xN, однако это всегда надо указывать, используя в качестве значения target константу GL_TEXTURE_1D. Существует одномерный аналог рассматриваемой команды - gluBuild1DMipmaps(), который отличается от двумерного отсутствием параметра height.

При использовании в сцене нескольких текстур, в OpenGL применяется подход, напоминающий создание списков изображений. Вначале, с помощью команды

void glGenTextures(GLsizei n, GLuint  *textures)

надо создать n идентификаторов для используемых текстур, которые будут записаны в массив textures. Перед началом определения свойств очередной текстуры следует вызвать команду

void glBindTexture(GLenum target, GLuint texture)

где target может принимать значения GL_TEXTURE_1D или GL_TEXTURE_2D, а параметр texture должен быть равен идентификатору той текстуры, к которой будут относиться последующие команды. Для того, чтобы в процессе рисования сделать текущей текстуру с некоторым идентификатором, достаточно опять вызвать команду glBindTexture() c соответствующим значением target и texture. Таким образом, команда glBindTexture() включает режим создания текстуры с идентификатором texture, если такая текстура еще не создана, либо режим ее использования, то есть делает эту текстуру текущей.

ПРИМЕЧАНИЕ

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

При наложении текстуры, как уже упоминалось, надо учитывать случай, когда размеры текстуры отличаются от размеров объекта, на который она накладывается. При этом возможно как растяжение, так и сжатие изображения, и то, как будут проводиться эти преобразования может серьезно повлиять на качество построенного изображения. Для определения положения точки на текстуре используется параметрическая система координат (s,t), причем значения s и t находятся в отрезке [0,1]. Для изменения различных параметров текстуры применяются команды:

void glTexParameter[i f](GLenum target, GLenum pname, GLenum param)
void glTexParameter[i f]v(GLenum target, GLenum pname, GLenum *params)

При этом target имеет аналогичный смысл, что и раньше, pname определяет, какое свойство будем менять, а с помощью param или params устанавливается новое значение. Возможные значения pname:

GL_TEXTURE_MIN_FILTER | параметр param определяет функцию, которая будет использоваться для сжатия текстуры. При значении GL_NEAREST будет использоваться один (ближайший), а при значении GL_LINEAR четыре ближайших элемента текстуры. Значение по умолчанию:GL_LINEAR.
GL_TEXTURE_MAG_FILTER | параметр param определяет функцию, которая будет использоваться для увеличения (растяжения) текстуры. При значении GL_NEAREST будет использоваться один (ближайший), а при значении GL_LINEAR четыре ближайших элемента текстуры. Значение по умолчанию:GL_LINEAR.
GL_TEXTURE_WRAP_S | параметр param устанавливает значение координаты s, если оно не входит в отрезок [0,1]. При значении GL_ REPEAT целая часть s отбрасывается, и в результате изображение размножается по поверхности. При значении GL_CLAMP используются краевые значения: 0 или 1, что удобно использовать, если на объект накладывается один образ. Значение по умолчанию:GL_REPEAT.
GL_TEXTURE_WRAP_T | аналогично предыдущему значению, только для координаты t.

Использование режима GL_NEAREST значительно повышает скорость наложения текстуры, однако при этом снижается качество, так как в отличие от GL_LINEAR интерполяция не производится.

Для того, чтобы определить, как текстура будет взаимодействовать с материалом, из которого сделан объект, используются команды

void glTexEnv[i f](GLenum target, GLenum pname, GLtype param)
void glTexEnv[i f]v(GLenum target, GLenum pname, GLtype *params)

Параметр target должен быть равен GL_TEXTURE_ENV, а в качестве pname рассмотрим только одно значение GL_TEXTURE_ENV_MODE, которое наиболее часто применяется.

Параметр param может быть равен:

GL_MODULATE | конечный цвет находится как произведение цвета точки на поверхности и цвета соответствующей ей точки на текстуре.
GL_REPLACE | в качестве конечного цвета используется цвет точки на текстуре.
GL_BLEND | конечный цвет находится как сумма цвета точки на поверхности и цвета соответствующей ей точки на текстуре с учетом их яркости.

Общий подход к созданию текстур:

/* нужное нам количество текстур */
#define NUM_TEXTURES 10
/* идентификаторы текстур */
int TextureIDs[NUM_TEXTURES];
/* образ текстуры */
AUX_RGBImageRec *pImage;
/*...*/
/* 1) получаем идентификаторы текстур */
glGenTextures(NUM_TEXTURES,TextureIDs);
/* 2) выбираем текстуру для модификации параметров */
glBindTexture(TextureIDs[i]); /* 0<=i<NUM_TEXTURES*/
/* 3) загружаем текстуру. Размеры текстуры - степень 2 */
pImage=dibImageLoad("texture.bmp");
if (Texture!=NULL)
{
  /* 4) передаем текстуру OpenGL и задаем параметры*/
  /* выравнивание по байту */
  glPixelStorei(GL_UNPACK_ALIGNMENT,1);
  gluBuildMipmaps(GL_TEXTURE_2D,GL_RGB, pImage->sizeX,
      pImage->sizeY, GL_RGB, GL_UNSIGNED_BYTE,
      pImage->data);
  glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,
      (float)GL_LINEAR);
  glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
      (float)GL_LINEAR);
  glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S,
      (float)GL_REPEAT);
  glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T,
      (float)GL_REPEAT);
  glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE,
      (float)GL_REPLACE);
  /* 5) удаляем исходное изображение.*/
  free(Texture);
}
else
 Error();

Координаты текстуры

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

Первый метод реализуется с помощью команд

void glTexCoord[1 2 3 4][s i f d](type coord)
void glTexCoord[1 2 3 4][s i f d]v(type *coord)

Чаще всего используется команды вида glTexCoord2..(type s, type t), задающие текущие координаты текстуры. Вообще, понятие текущих координат текстуры аналогично понятиям текущего цвета и текущей нормали, и является атрибутом вершины. Однако даже для куба нахождение соответствующих координат текстуры является довольно трудоемким занятием, поэтому в библиотеке GLU помимо команд, проводящих построение таких примитивов, как сфера, цилиндр и диск, предусмотрено также наложение на них текстур. Для этого достаточно вызвать команду

void gluQuadricTexture(GLUquadricObj  *quadObject, GLboolean textureCoords)

с параметром textureCoords равным GL_TRUE, и тогда текущая текстура будет автоматически накладываться на примитив.

Второй метод реализуется с помощью команд

void glTexGen[i f d](GLenum coord, GLenum pname, GLtype param)
void glTexGen[i f d]v(GLenum coord, GLenum pname, const GLtype *params)

Параметр coord определяет для какой координаты задается формула и может принимать значение GL_S,GL_T; pname может быть равен одному из следующих значений:

GL_TEXTURE_GEN_MODE определяет функцию для наложения текстуры.

В этом случае аргумент param принимает значения:

GL_OBJECT_LINEAR | значение соответствующей текстурной координаты определяется расстоянием до плоскости, задаваемой с помощью значения pname GL_OBJECT_PLANE (см. ниже). Формула выглядит следующим образом: g=x*xp+y*yp+z*zp+w*wp, где g-соответствующая текстурная координата ( s или p), x, y, z, w - координаты соответствующей точки. xp, yp, zp, wp - коэффициенты уравнения плоскости. В формуле используются координаты объекта.
GL_EYE_LINEAR | аналогично предыдущему значению, только в формуле используются видовые координаты. Т.е. координаты текстуры объекта в этом случае зависят от положения этого объекта.
GL_SPHERE_MAP | позволяет эмулировать отражение от поверхности объекта. Текстура как бы "оборачивается" вокруг объекта. Для данного метода используются видовые координаты и необходимо задание нормалей.
GL_OBJECT_PLANE | позволяет задать плоскость, расстояние до которой будет использоваться при генерации координат, если установлен режим GL_OBJECT_LINEAR. В этом случае параметр params является указателем на массив из четырех коэффициентов уравнения плоскости.
GL_EYE_PLANE | аналогично предыдущему значению. Позволяет задать плоскость для режима GL_EYE_LINEAR

Для установки автоматического режима задания текстурных координат необходимо вызвать команду glEnable с параметром GL_TEXTURE_GEN_S или GL_TEXTURE_GEN_P.

Пример:

Рассмотрим, как можно задать зеркальную текстуру. При таком наложении текстуры изображение будет как бы отражаться от поверхности объекта, вызывая интересный оптический эффект. Для этого сначала надо создать два целочисленных массива коэффициентов s_coeffs и t_coeffs со значениями (1,0,0,1) и (0,1,0,1) соответственно, а затем вызвать команды:

glEnable(GL_TEXTURE_GEN_S);
glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
glTexGendv(GL_S, GL_EYE_PLANE, s_coeffs);

и такие же команды для координаты t с соответствующими изменениями.

Создание спецэффектов

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

Туман

Добавление тумана в сцену может повысить реализм, а иногда и скрыть некоторые артефакты, которые появляются, когда в сцене присутствуют отдаленные объекты.

Туман в OpenGL реализуется путем изменения цвета объектов в сцене в зависимости от их глубины (расстояния от точки наблюдения).

Для включения тумана необходимо вызвать команду glEnable(GL_FOG).

Способ вычисления интенсивности тумана можно определить с помощью команд

void glFog[if](enum pname, T param) ;
void glFog[if]v(enum pname, T params) ;

Аргумент pname может принимать следующие значения:

GL_FOG_MODE в этом случае аргумент param определяет формулу, по которой будет вычисляться интенсивность тумана в точке.

param может принимать значения

GL_EXP | Интенсивность вычисляется по формуле f=exp(-d*z)
GL_EXP2 | Интенсивность вычисляется по формуле f=exp(-(d*z))
GL_LINEAR | Интенсивность вычисляется по формуле f=e-z/e-s

В этих формулах z обозначает расстояние от точки, в которой вычисляется интенсивность тумана, до точки наблюдения.

Коэффициенты d,e,s задаются с помощью следующих значений аргумента pname

GL_FOG_DENSITY | param определяет коээфициент d
GL_FOG_START | param определяет коэффициент s
GL_FOG_END | param определяет коэффициент e

Цвет тумана задается с помощью аргумента pname, равного

GL_FOG_COLOR в этом случае params - указатель на массив из 4х компонент цвета.

Пример:

Glfloat FogColor[4]={0.5,0.5,0.5,1};
glEnable(GL_FOG);
glFogi(GL_FOG_MODE,GL_LINEAR);
glFogf(GL_FOG_START,20.0);
glFogf(GL_FOG_END,100.0);
glFogfv(GL_FOG_COLOR,FogColor);

Прозрачность

Прозрачность позволяет использовать полупрозрачные объекты в сцене, что может значительно повысить реалистичность изображения.

В OpenGL прозрачность реализуется с помощью специального режима смешения цветов (blending). Алгоритм смешения комбинирует цвета входящих пикселей (RGBA) с цветами соответствующих пикселей, уже хранящихся в буфере кадра.

Режим включается с помощью команды glEnable(GL_BLEND).

Определить параметры смешения можно с помощью команды:

void glBlendFunc(enum src,enum dst)

Параметр src определяет, как получить коэффициент k1 исходного цвета пикселя, a dst определяет способ получения коэффициента k2 для цвета в буфере кадра. Для получения результирующего цвета используется следующая формула: res=с*k1+c*k2, где с - цвет исходного пикселя, c - цвет пикселя в буфере кадра. ( res, k1, k2, с c - векторы!).

Приведем наиболее часто используемые значения агрументов src и dst.

GL_SRC_ALPHA | k=(A,A,A,A)
GL_SRC_ONE_MINUS_ALPHA | k=(1,1,1,1)-(A,A,A,A)
GL_DST_COLOR | k=(R,G,B)
GL_ONE_MINUS_DST_COLOR | k=(1,1,1,1)- (R,G,B)
GL_DST_ALPHA | k=(A,A,A,A)
GL_DST_ONE_MINUS_ALPHA | k=(1,1,1,1)-(A,A,A,A)
GL_SRC_COLOR | k=(R,G,B)
GL_ONE_MINUS_SRC_COLOR | k=(1,1,1,1)- (R,G,B)

Пример:

Предположим, мы хотим реализовать вывод прозрачных объектов. Коэффициент прозрачности задается alpha-компонентой цвета. alpha, равное 1 - непрозрачный объект; равное 0 - невидимый. Для реализации служит следующий код:

glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA,GL_SRC_ONE_MINUS_ALPHA);

ПРИМЕЧАНИЕ

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

Все прозрачные объекты выводятся после непрозрачных.

При выводе объекты с прозрачностью должны быть упорядочены по уменьшению глубины, т.е. выводиться начиная с наиболее отдаленных.

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

Буфер накопления (accumulation buffer) - это дополнительный внутренний буфер OpenGL. В нем можно сохранять визуализированное изображение, применяя при этом попиксельно специальные операции.

Изображение берется из буфера, выбранного на чтение командой

void glReadBuffer(enum buf)

Аргумент buf определяет буфер для чтения. Значения buf, равные GL_BACK, GL_FRONT, определяют соответствующие буфера для чтения.

Применяя различные операции, описанные ниже, можно как бы понемногу накапливать изображение в буфере.

Затем картинка переносится из буфера накопления в буфер, выбранный на запись командой

void glDrawBuffer(enum buf)

Значение buf аналогично значению соответствующего аргумента в команде glReadBuffer.

Все операции с буфером накопления контролируются командой

void glAccum(enum op, GLfloat value)

Аргумент op задает операцию над пикселями и может принимать следующие значения:

GL_LOAD | Пиксель выбирается из буфера, выбранного на чтение, его значение умножается на value и заносится в буфер накопления.
GL_ACCUM | Аналогично предыдущему, но полученное после умножения значение складывается с уже имеющимся в буфере.
GL_MULT | Эта операция умножает значение каждого пикселя в буфере накопления на value .
GL_ADD | Аналогично предыдущему, только вместо умножения используется сложение.
GL_RETURN | Изображение переносится из буфера накопления в буфер, выбранный для записи. Перед этим значение каждого пикселя умножается на value.

Для использования буфера накопления нет необходимости вызывать какие-либо команды glEnable. Достаточно только иметь сам буфер.

В качестве примера использования буфера накопления рассмотрим задачу устранения лестничного эффекта (antialiasing).

Алгоритм ее решения сразу для всей сцены таков:

Для каждого кадра выводим сцену несколько раз, каждый раз немного смещая камеру относительно начального положения (положения камер, например, могут образовывать окружность). Все сцены сохраняем в буфере накопления с коэффициентом 1/n, где n - число сцен для каждого кадра. Чем больше таких сцен (antialiasing samples) - тем хуже производительность, но лучше качество.

for(i=0;i<samples_count;++i)
/* обычно samples_count лежит в пределах от 5 до 10 */
{
  ShiftCamera(i); /* сдвигаем камеру */
  RenderScene();
  if (i==0)
   /* на первой итерации загружаем изображение */
   glAccum(GL_LOAD,1/(float)samples_count);
  else
   /* добавляем к уже существующему */
   glAccum(GL_ADD,1/(float)samples_count);
}
/* Пишем то, что получилось, назад в исходный буфер */
glAccum(GL_RETURN,1.0);

ПРИМЕЧАНИЕ

Буфер накопления редко реализуется аппаратно. Поэтому использование устранения ступенчатости сразу для всей сцены практически несовместимо с визуализацией динамических изображений с приемлемой частотой вывода кадров (frame rate).
Трафаретный буфер

При выводе пикселей в буфер кадра иногда возникает необходимость выводить не все пиксели, а только некоторое их подмножество, т.е. как бы наложить трафарет на изображение. Для этого OpenGL предоставляет так называемый трафаретный буфер (stencil buffer). Кроме наложения трафарета, этот буфер предоставляет еще несколько интересных возможностей.

Прежде чем поместить пиксель в буфер кадра, механизм визуализации OpenGL позволяет выполнить сравнение (тест) между заданным значением и значением в трафаретном буфере. Если тест проходит, пиксель визуализируется в буфере кадра.

Механизм сравнения контролируется следующими командами:

void glStencilFunc(enum func, int ref, uint mask)
void glStencilOp(enum sfail, enum dpfail, enum  dppass)

Аргумент ref команды StencilFunc задает значение для сравнения. Он должен принимать значение от 0 до 2-1. s - число бит на точку в трафаретном буфере.

С помощью аргумента func задается функция сравнения. Он может принимать следующие значения:

GL_NEVER | тест никогда не проходит, т.е всегда возвращает false
GL_ALWAYS | тест проходит всегда.
GL_LESS, GL_LEQUAL, GL_EQUAL, GL_GEQUAL, GL_GREATE, GL_NOTEQUAL | тест проходит в случае, если ref соответственно меньше значения в трафаретном буфере, меньше либо равен, равен, больше, больше либо равен или не равен.

Аргумент mask задает маску для значений. Т.е. в итоге для трафаретного теста получаем следующую формулу: ((ref AND mask) op (svalue AND mask))

Команда StencilOp предназначена для определения действий над пикселем трафаретного буфера в случае положительного или отрицательного результата теста.

Аргумент sfail задает действие в случае отрицательного результата теста, и может принимать следующие значения:

GL_KEEP, GL_ZERO, GL_REPLACE, GL_INCR, GL_DECR, GL_INVERT соответственно сохраняет значение в трафаретном буфере, обнуляет его, заменяет на заданное значение (ref), увеличивает, уменьшает или побитово инвертирует.

Аргументы dpfail определяют действия в случае отрицательного результата теста на глубину в z-буфере. dppass задает действие в случае положительного результата этого теста. Аргументы принимают те же значения, что и аргумент sfail. По умолчанию все три параметра установлены на GL_KEEP.

Для включения трафаретного теста необходимо выполнить команду glEnable(GL_STENCIL_TEST);

Трафаретный тест используется при создании таких спецэффектов, как тени, отражения, плавные переходы из одной картинки в другую, создания конструктивной геометрии (CSG) и др.

Пример использования трафаретного теста при создании теней описан в Приложении.

[Текст "Приложения" к сожалению не влезает даже в расширенный выпуск и всех интересующихся отсылаю к оригиналу статьи на RSDN.]

Это все на сегодня. Пока!

Алекс Jenter jenter@rsdn.ru
Duisburg, 2001. Публикуемые в рассылке материалы принадлежат сайту RSDN.


ПРОГРАММИРОВАНИЕ НА V I S U A L C + +
РАССЫЛКА САЙТА
RSDN.RU
Выпуск No. 68 от 17 марта 2002 г.

РАССЫЛКА ЯВЛЯЕТСЯ ЧАСТЬЮ ПРОЕКТА RSDN , НА САЙТЕ КОТОРОГО ВСЕГДА МОЖНО НАЙТИ ВСЮ НЕОБХОДИМУЮ РАЗРАБОТЧИКУ ИНФОРМАЦИЮ, СТАТЬИ, ФОРУМЫ, РЕСУРСЫ, ПОЛНЫЙ АРХИВ ПРЕДЫДУЩИХ ВЫПУСКОВ РАССЫЛКИ И МНОГОЕ ДРУГОЕ.

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

CТАТЬЯ

Использование атрибутов в среде .NET

Автор: Алифанов Андрей
АТРИБУТ. Необходимый, постоянный признак, принадлежность.
"Толковый словарь русского языка", С.И. Ожегов

Демонстрационный проект

Введение

Если вы когда-либо программировали на C++, вам должны быть знакомы определения, такие как public и private, предоставляющие дополнительную информацию о членах класса. Эти ключевые слова задают поведение членов класса, описывая их доступность извне. Так как компиляторы распознают только предопределенные ключевые слова, вы не имеете возможности создавать свои собственные.

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

Когда вы компилируете свой код, он преобразуется в Microsoft Intermediate Language (MSIL) и помещается в файл формата Portable Executable (PE) вместе с метаданными, сгенерированными компилятором. Атрибуты позволяют добавить к метаданным дополнительную информацию, которая затем может извлекаться при помощи механизма рефлексии. Компилятор создает атрибуты, когда вы объявляете экземпляры специальных классов, наследующих от System.Attribute.

.NET Framework широко использует атрибуты. Атрибуты описывают правила сериализации данных, управляют безопасностью и ограничивают оптимизацию JIT-компиляторов для облегчения отладки кода. Атрибуты также могут содержать имя файла или автора, или управлять видимостью элементов управления и классов при разработке форм пользовательского интерфейса.

Вы можете использовать атрибуты для произвольного комментирования кода и управления поведением компонентов. Атрибуты позволяют добавлять описательные элементы в C#, управляемые расширения для C++, Microsoft Visual Basic.NET или в любой другой язык, поддерживающий CLR, без необходимости переписывать компилятор.

Кроме того, атрибуты можно использовать в ATL проектах, но это тема уже другой статьи.

Применение атрибутов

Большинство атрибутов применяется к таким элементам языка как классы, методы, поля и свойства. Но некоторые атрибуты являются глобальными – они воздействуют на всю сборку или модуль. Глобальные атрибуты в текстах программ объявляются после using директив верхнего уровня перед определениями типов и пространств имен. Они могут использоваться в разных исходных файлах одной программы.

Применение атрибутов на уровне классов и методов

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

  1. Определяется новый или берется существующий в .NET Framework атрибут
  2. Инициализируется конкретный экземпляр атрибута с помощью вызова конструктора атрибута.

Атрибут помещается в метаданные при компиляции кода и становится доступен CLR и любым инструментальным средствам и приложениям через механизмы рефлексии.

По соглашению, имена всех атрибутов оканчиваются словом Attribute. Однако, языки из VisualStudio.NET, не требуют задания полного имени атрибута. Например, если нужно инициализировать атрибут System.ObsoleteAttribute, достаточно написать Obsolete.

Следующий пример показывает, как использовать атрибут System.ObsoleteAttribute, помечающий код как устаревший. Атрибуту передается строка "Будет удалено в следующей версии". Этот атрибут заставляет компилятор выдать переданную строку как предупреждение при компиляции помеченного кода.

C#

using System;
public class MainApp
{
  public static void Main()
  {
     //На этой строке компилятор выдаст предупреждение.
     int MyInt = Add(2,2);
  }

  //В C# атрибуты задаются в квадратных скобках.
  //Этот атрибут применяется только к методу Add.
  [Obsolete("В следующей версии метод будет удален")]
  public static int Add(int a, int b)
  {
    return (a + b);
  }
}

MC++

#using <mscorlib.dll>
using namespace System;
int Add(int a, int b);
void main(void)
{
  //На этой строке компилятор выдаст предупреждение.
  int MyInt = Add(2, 2);
  return;
}

//В MC++ атрибуты задаются в квадратных скобках.

//Этот атрибут применяется только к методу Add.
[Obsolete(S"В следующей версии метод будет удален")]
int Add(int a, int b)
{
  return (a + b);
}

Visual Basic.NET

Imports System
Public Module main
   Sub Main()
   'На этой строке компилятор выдаст предупреждение.
   Dim MyInt as Integer = Add(2,2)
   End Sub

  ' В Visual Basic.NET атрибуты задаются между скобками < и >.
  ' Этот атрибут применяется только к методу Add.
  Function <Obsolete("В следующей версии метод будет удален")>_
  Add(a as Integer, b as Integer) as Integer
    Add = a + b
  End Function
End Module

Применение атрибутов на уровне сборок

Для применения атрибутов на уровне сборок используется ключевое слово Assembly. Следующий пример показывает, как используется атрибут AssemblyNameAttribute:

C#

using System.Reflection;
[assembly:AssemblyName("Моя сборка")]

MC++

using namespace System::Reflection;
[assembly:AssemblyName(S"Моя сборка")];

Visual Basic.NET

Imports System.Reflection
<Assembly:AssemblyName("Моя сборка")>

При компиляции кода строка "Моя сборка" помещается в манифест сборки в секции метаданных. Этот атрибут можно увидеть с помощью дизассемблера MSIL (Ildasm.exe) или с помощью пользовательских средств.

Применение атрибутов на уровне модулей

Для применения атрибутов на уровне модулей используется ключевое слово Module, в остальном все как на уровне сборок.

Пользовательские атрибуты

Чтобы разрабатывать собственные атрибуты, не нужно изучать что-то принципиально новое. Если вы знакомы с объектно-ориентированным программированием и знаете, как разрабатывать классы, вы знаете уже практически все. Пользовательские атрибуты – это классы, тем или иным образом наследующие от System.Attribute. Также как и все другие классы, пользовательские атрибуты содержат методы для записи и чтения данных. Рассмотрим процесс создания пользовательского атрибута по шагам.

Применение атрибута AttributeUsageAttribute

Объявление пользовательского атрибута начинается с AttributeUsageAttribute, который задает ключевые характеристики нового класса. Например, он определяет, может ли атрибут наследоваться другими классами, или к каким элементам он может применяться. Следующий фрагмент кода иллюстрирует применение атрибута AttributeUsageAttribute

C#

[AttributeUsage(AttributeTargets.All, Inherited = false,
 AllowMultiple = true)]

MC++

[AttributeUsage(AttributeTargets::All, Inherited = false,
 AllowMultiple = true)]

Visual Basic.NET

<AttributeUsage(AttributeTargets.All, Inherited := False,
 AllowMultiple := true)>

Класс System.AttributeUsageAttribute содержит три члена, которые важны для создания пользовательских атрибутов: AttributeTargets, Inherited и AllowMultiple.

Поле AttributeTargets

В предыдущем примере используется флаг AttributeTargets.All. Этот флаг означает, что данный атрибут может применяться к любым элементам программы. С другой стороны, можно задать флаг AttributeTargets.Class, означающий, что атрибут применяется только к классам, или AttributeTargets.Method – для методов классов и интерфейсов. Подобным образом можно применять и пользовательские атрибуты.

Также можно использовать несколько экземпляров атрибута AttributeTargets. В следующем примере показано, как пользовательский атрибут может применяться к любому классу или методу:

C#

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]

MC++

[AttributeUsage(AttributeTargets::Class | AttributeTargets::Method)]

Visual Basic.NET

<AttributeUsage(AttributeTargets.Class BitOr AttributeTargets.Method)>

Свойство Inherited

Это свойство определяет, будет ли атрибут наследоваться классами, наследниками того, к которому этот атрибут применен. Это свойство может принимать два значения: true или false.

C#

// По умолчанию Inherited = true.
public class MyAttribute : Attribute
{
}
// Явно задается false.

[AttributeUsage(Inherited = false)]
public class YourAttribute : Attribute
{
}

MC++

// По умолчанию Inherited = true.
public __gc class MyAttribute : public System::Attribute
{
}
// Явно задается false.

[AttributeUsage(Inherited = false)]
public __gc class YourAttribute : public System::Attribute
{
}

Visual Basic.NET

' По умолчанию Inherited := true.
Public Class _
<AttributeUsage(AttributeTargets.All, Inherited := True)> MyAttribute
  Inherits Attribute
End Class
Public Class _
<AttributeUsage(AttributeTargets.All, Inherited := False)> YourAttribute
  Inherits Attribute
End Class

Вышеописанные атрибуты затем применяются к методу класса MyClass:

C#

public class MyClass
{
  // В C# несколько атрибутов могут определяться в разных блоках,

  // ограниченных скобками или в одном блоке – через запятую.

  // Порядок следования атрибутов неважен.

  [MyAttribute][YourAttribute]
  public void MyMethod() {
    //...
  }
}

MC++

public __gc class MyClass
{
public:
  // В MC++ несколько атрибутов могут определяться в разных блоках,

  // ограниченных скобками или в одном блоке – через запятую.

  // Порядок следования атрибутов неважен.

  [MyAttribute][YourAttribute]
  void MyMethod() {
    //...
  }
}

Visual Basic.NET

' В Microsoft Visual Basic.NET несколько атрибутов разделяются запятыми.

' Порядок следования атрибутов неважен.

Public Class MyClass
  Public Sub <MyAttribute, YourAttribute> MyMethod()
    '...
  End Sub
End Class

И, наконец, рассмотрим класс YourClass - наследник MyClass. С методом MyMethod этого класса будет связан только атрибут MyAttribute.

C#

public class YourClass : MyClass
{
  // Этот метод имеет только атрибут MyAttribute.
  public void MyMethod()
  {
    //...

  }
}

MC++

public __gc class YourClass : public MyClass
{
public:
  // Этот метод имеет только атрибут MyAttribute.
  void MyMethod()
  {
    //...

  }
}

Visual Basic.NET

Public Class YourClass
   Inherits MyClass
' Этот метод имеет только атрибут MyAttribute.
  Public Sub MyMethod()
    '...
  End Sub
End Class

Свойство AllowMultiple

Это свойство показывает, может ли атрибут применяться многократно к одному элементу. По умолчанию оно равно false, что значит – атрибут может использоваться только один раз. Рассмотрим следующий пример:

C#

// По умолчанию AllowMultiple = false.
public class MyAttribute : Attribute
{
}

[AttributeUsage(AllowMultiple = true)]
public class YourAttribute : Attribute
{
}

MC++

// По умолчанию AllowMultiple = false.
public __gc class MyAttribute : public System::Attribute
{
}

[AttributeUsage(AllowMultiple = true)]
public __gc class YourAttribute : public System::Attribute
{
}

Visual Basic.NET

' По умолчанию AllowMultiple = false.
Public Class _
<AttributeUsage(AttributeTargets.Method)> MyAttribute
    Inherits Attribute
End Class

Public Class _
<AttributeUsage(AttributeTargets.Method,

 AllowMultiple := True)> YourAttribute
    Inherits Attribute
End Class

Если используется несколько экземпляров атрибутов, MyAttribute заставляет компилятор выдать сообщение об ошибке. Следующий фрагмент кода иллюстрирует правильное использование атрибута YourAttribute и неправильное – MyAttribute:

C#

public class MyClass
{
  // Ошибка -  дублирование не разрешено.
  [MyAttribute, MyAttribute]
  public void MyMethod()
  {
    //...
  }

  // Это допустимо.
  [YourAttribute, YourAttribute]
  public void YourMethod()
  {
    //...
  }
}

MC++

public __gc class MyClass
{
public:
  // Ошибка -  дублирование не разрешено.
  [MyAttribute, MyAttribute]
  void MyMethod()
  {
    //...
  }

  // Это допустимо.
  [YourAttribute, YourAttribute]
  void YourMethod()
  {
    //...
  }
}

Visual Basic.NET

Public Class MyClass
    ' Ошибка -  дублирование не разрешено.
    Public Sub <MyAttribute, MyAttribute> MyMethod()
        '...
    End Sub

    ' Это допустимо.
    Public Sub <YourAttribute, YourAttribute> YourMethod()
        '...
    End Sub
End Class

Если свойства AllowMultiple и Inherited установлены в true, класс может наследовать атрибут и иметь еще экземпляры, примененные непосредственно к нему. Если же свойство AllowMultiple равно false, значения атрибутов родительского класса будут переписаны значениями этого же атрибута класса-наследника.

Типы данных, допустимые в атрибутах

Атрибут может содержать поля следующих типов:

  • Bool
  • Byte
  • Char
  • Double
  • Float
  • Int
  • Long
  • Short
  • String
  • Object
  • System.Type
  • Открытые перечислимые типы, вложенные (если вложены) в открытые типы

Попытка использовать в классе, реализующем атрибут другие типы, приводит к ошибкам компиляции.

Определение атрибутивного класса

Теперь можно приступить к определению самого класса. Это определение выглядит подобно определению обычного класса, что демонстрирует следующий пример:

C#

// Этот атрибут может применяться только к методам
public class MyAttribute : System.Attribute
{
  // ...
}

MC++

// Этот атрибут может применяться только к методам
public __gc class MyAttribute : System.Attribute
{
  // ...
}

Visual Basic.NET

' Этот атрибут может применяться только к методам
Public Class <AttributeUsage(AttributeTargets.Method)> MyAttribute
    Inherits System.Attribute
    ' ...
End Class

Этот пример показывает следующие положения:

  • Атрибутивные классы должны объявляться как открытые
  • По соглашению, имена классов должны заканчиваться словом Attribute. Хотя это и необязательно, рекомендуется поступать так для улучшения читаемости текста. При использовании атрибута это слово необязательно.
  • Все атрибутивные классы должны, так или иначе, наследовать от System.Attribute.
  • В Microsoft Visual Basic все пользовательские атрибутивные классы должны иметь атрибут AttributeUsageAttribute.
Определение конструкторов

Атрибуты инициализируются конструкторами, так же как обычные классы. Следующий фрагмент кода иллюстрирует типичный конструктор атрибута. Этот открытый конструктор принимает один параметр и инициализирует переменную класса.

C#

public MyAttribute(bool myvalue)
{
   this.myvalue = myvalue;
}

MC++

public:

MyAttribute(bool myvalue)
{
   this->myvalue = myvalue;
}

Visual Basic.NET

Public Sub New(newvalue As Boolean)
    Me.myvalue = newvalue
End Sub

Конструкторы можно перегружать, чтобы принимать различные комбинации параметров. Если для атрибутивного класса определены свойства, для инициализации можно использовать комбинацию позиционных и именованных параметров. Обычно все обязательные параметры объявляются как позиционные, а необязательные как именованные.

Следующий пример показывает примеры использования параметризованного конструктора для инициализации атрибута. Здесь предполагается, что атрибут имеет обязательный параметр типа Boolean и необязательный типа String.

C#

// Один обязательный (позиционный) и один
// необязательный (именованный) параметры.
[MyAttribute(false, OptionalParameter = "дополнительные данные")]
// Один обязательный (позиционный) параметр.
[MyAttribute(false)]

MC++

// Один обязательный (позиционный) и один необязательный
 //(именованный) параметры.
[MyAttribute(false, OptionalParameter = S"дополнительные данные")]
// Один обязательный (позиционный) параметр.
[MyAttribute(false)]

Visual Basic.NET

' Один обязательный (позиционный) и один необязательный
 '(именованный) параметры.
<MyAttribute(False, OptionalParameter := "дополнительные данные")>
' ...
' Один обязательный (позиционный) параметр.
<MyAttribute(False)>

Параметры, определенные как свойства, могут передаваться в произвольном порядке. Но обязательные параметры должны передаваться в том порядке, в котором они описаны в конструкторе. Следующий фрагмент кода показывает, как необязательный параметр может передаваться перед обязательным.

C#

// Именованный параметр помещается перед позиционным.
[MyAttribute(OptionalParameter = "дополнительные данные", false)]

MC++

// Именованный параметр помещается перед позиционным.
[MyAttribute(OptionalParameter = S"дополнительные данные", false)]

Visual Basic.NET

' Именованный параметр помещается перед позиционным.
<MyAttribute(OptionalParameter := "дополнительные данные" , False)>

Определение свойств

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

C#

public bool MyProperty
{
  get { return this.myvalue; }
  set { this.myvalue = value; }
}

MC++

public:
__property bool get_MyProperty() { return myvalue; }
__property void set_MyProperty(bool value) { myvalue = value; }

protected:
  bool myvalue;
}

Visual Basic.NET

Public Property MyProperty As Boolean
  Get
    Return Me.myvalue
  End Get
  Set
    Me.myvalue = value
  End Set
End Property

Пример пользовательского атрибута

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

C#

[AttributeUsage(AttributeTargets.All)]
public class DeveloperAttribute : System.Attribute
{
  // Закрытые поля.
  private string name;
  private string level;
  private bool reviewed;

  // Конструктор принимает два обязательных параметра: имя и уровень.
  public DeveloperAttribute(string name, string level)
  {
    this.name = name;
    this.level = level;
    this.reviewed = false;
  }

  // Свойство Name.
  // Только для чтения.
  public virtual string Name
  {
    get { return name; }
  }

  // Свойство Level.
  // Только для чтения.
  public virtual string Level
  {
    get { return level; }
  }

  // Свойство Reviewed.
  // Чтение / Запись.
  public virtual bool Reviewed
  {
    get { return reviewed; }
    set { reviewed = value; }
  }
}

MC++

#using <mscorlib.dll>
[AttributeUsage(AttributeTargets::All)]
public __gc class DeveloperAttribute : public System::Attribute
{
private:
  // Закрытые поля.
  String* name;
  String* level;
  Boolean reviewed;

public:
  // Конструктор принимает два обязательных параметра: имя и уровень.
  DeveloperAttribute(String* name, String* level)
  {
    this->name = name;
    this->level = level;
    this->reviewed = false;
  }

  // Свойство Name.
  // Только для чтения.
  __property virtual String* get_Name() { return name; }

  // Свойство Level.
  // Только для чтения.
  __property virtual String* get_Level() { return level; }

  // Свойство Reviewed.
  // Чтение / Запись.
  __property virtual Boolean get_Reviewed() { return reviewed; }
  __property virtual void set_Reviewed(Boolean value) { reviewed = value; }
}

Visual Basic.NET

Public Class <AttributeUsage(AttributeTargets.All)> DeveloperAttribute
  Inherits System.Attribute

  ' Закрытые поля.
  Private name As String
  Private level As String
  Private reviewed As Boolean

  ' Конструктор принимает два обязательных параметра: имя и уровень.
  Public Sub New(name As String, level As String)
    Me.name = name
    Me.level = level
    Me.reviewed = False
  End Sub

  ' Свойство Name.
  ' Только для чтения.
  Public Overridable ReadOnly Property Name() As String
    Get
      Return name
    End Get
  End Property

  ' Свойство Level.
  ' Только для чтения.
  Public Overridable ReadOnly Property Level() As String
    Get
      Return level
    End Get
  End Property

  ' Свойство Reviewed.
  ' Чтение / Запись.
  Public Overridable Property Reviewed() As Boolean
    Get
      Return reviewed
    End Get
    Set
      reviewed = value
    End Set
  End Property
End Class

Применять этот атрибут можно, используя как полное имя DeveloperAttribute, так и сокращенное – Developer:

C#

[Developer("Иван Семенов", "1")]
[Developer("Иван Семенов", "1", Reviewed = true)]

MC++

[Developer(S"Иван Семенов", S"1")]
[Developer(S"Иван Семенов", S"1", Reviewed = true)]

Visual Basic.NET

<Developer("Иван Семенов", "1")>
<Developer("Иван Семенов", "1", Reviewed := True)>

В первом примере показано применение атрибута с одним обязательным параметром, а во втором – с обоими типами параметров.

Доступ к информации, хранящейся в атрибутах

Теперь настало время рассмотреть механизм получения атрибутов, ведь мало научиться сохранять свои атрибуты в метаданных, важно еще и уметь получать и использовать их. К счастью, получение пользовательского атрибута – простая задача. Сначала объявляется переменная с типом атрибута, который нужно получить, затем она инициализируется с помощью вызова метода Attribute.GetCustomAttribute. Все, теперь можно использовать любые доступные свойства атрибута.

Получение одиночного атрибута

В следующем примере атрибут DeveloperAttribute (рассмотренный выше) применяется к классу MainApp в целом. Метод GetAttribute использует Attribyte.GetCustomAttribute для получения состояния атрибута DeveloperAttribute перед тем, как вывести информацию на консоль.

C#

using System;

[Developer("Иван Семенов", "42", Reviewed = true)]
class MainApp
{
  public static void Main()
  {
    // Вызвать функцию получения и отображения атрибута.
    GetAttribute(typeof(MainApp));
  }

  public static void GetAttribute(Type t)
  {
    // Получить атрибут.
    DeveloperAttribute MyAttribute =
      (DeveloperAttribute)<
     BR >
       Attribute.GetCustomAttribute(t, typeof(DeveloperAttribute));

    if (MyAttribute == null)
    {
      Console.WriteLine("Атрибут не найден.");
    }
    else
    {
      // Получить поле Имя.
      Console.WriteLine("Имя: {0}." , MyAttribute.Name);
      // Получить поле Уровень.
      Console.WriteLine("Уровень: {0}." , MyAttribute.Level);
      // Получить поле Проверено.
      Console.WriteLine("Проверено: {0}." , MyAttribute.Reviewed);
    }
  }
}

MC++

#using <mscorlib.dll>
using namespace System;

[Developer(S"Иван Семенов", S"42", Reviewed = true)]
public __gc class MainApp
{
public:
  static void GetAttribute(Type* t)
  {
    // Получить атрибут.
    DeveloperAttribute* MyAttribute =
      __try_cast<DeveloperAttribute*>
(Attribute::GetCustomAttribute(t, __typeof(DeveloperAttribute)));

    if (MyAttribute == 0)
      Console::WriteLine(S"Атрибут не найден.");
    else {
      // Получить поле Имя.
      Console::WriteLine(S"Имя: {0}." , MyAttribute->Name);
      // Получить поле Уровень.
      Console::WriteLine(S"Уровень: {0}." , MyAttribute->Level);
      // Получить поле Проверено.
      Console::WriteLine(S"Проверено: {0}." , MyAttribute->Reviewed);
    }
  }
};

void main()
{
  // Вызвать функцию получения и отображения атрибута.
  MainApp::GetAttribute(__typeof(MainApp));
}

Visual Basic.NET

Imports System
Class <Developer("Иван Семенов", "42", Reviewed := True)> MainApp
  Public Shared Sub Main()
    ' Вызвать функцию получения и отображения атрибута.
    GetAttribute(GetType(MainApp))
  End Sub

  Public Shared Sub GetAttribute(t As Type)
    ' Получить атрибут.
    Dim MyAttribute As DeveloperAttribute = _
      CType(Attribute.GetCustomAttribute
(t, GetType(DeveloperAttribute)), DeveloperAttribute)

    If MyAttribute Is Nothing Then
      Console.WriteLine("Атрибут не найден.")
    Else
      ' Получить поле Имя.
      Console.WriteLine("Имя: {0}.", MyAttribute.Name)
      ' Получить поле Уровень.
      Console.WriteLine("Уровень: {0}.", MyAttribute.Level)
      ' Получить поле Проверено.
      Console.WriteLine("Проверено: {0}.", MyAttribute.Reviewed)
    End If
  End Sub
End Class

При запуске эта программа выдает на консоль следующие строки:

Имя: Иван Семенов
Уровень: 42
Проверено: True

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

Получение списка однотипных атрибутов

В предыдущем примере ссылки на класс и атрибут передавались в метод GetCustomAttribute. Этот код прекрасно работает, если на уровне класса определен только один атрибут. Но если на том же уровне определено несколько однотипных атрибутов, этот метод вернет не всю информацию. В таких случаях нужно использовать метод Attribute.GetCustomAttributes, который возвращает массив атрибутов. Например, если на уровне класса определены два экземпляра атрибута DeveloperAttribute, можно модифицировать метод GetAttribute, чтобы получить оба.

Как это сделать, показано в следующем примере:

C#

public static void GetAttribute(Type t)
{
  // Получить атрибут.
  DeveloperAttribute[] MyAttribute =
    (DeveloperAttribute[]) Attribute.GetCustomAttributes<
   BR >
     (t, typeof(DeveloperAttribute));

  if (MyAttribute == null)
    Console.WriteLine("Атрибут не найден.");
  else
    for (int i = 0 ; i < MyAttribute.Length ; i++) {
      // Получить поле Имя.
      Console.WriteLine("Имя: {0}." , MyAttribute[i].Name);
      // Получить поле Уровень.
      Console.WriteLine("Уровень: {0}." , MyAttribute[i].Level);
      // Получить поле Проверено.
      Console.WriteLine("Проверено: {0}.", MyAttribute[i].Reviewed);
    }
}

MC++

public:
  static void GetAttribute(Type* t)
  {
    // Получить атрибут.
    DeveloperAttribute* MyAttribute __gc[] =
      __try_cast<DeveloperAttribute* __gc[]>
(Attribute::GetCustomAttributes(t, __typeof(DeveloperAttribute)));

    if (MyAttribute == 0)
      Console::WriteLine(S"Атрибут не найден.");
    else
      for (int i = 0; i < MyAttribute.Length; i++) {
        // Получить поле Имя.
        Console::WriteLine(S"Имя: {0}." , MyAttribute[i]->Name);
        // Получить поле Уровень.
        Console::WriteLine(S"Уровень: {0}." , MyAttribute[i]->Level);
        // Получить поле Проверено.
        Console::WriteLine(S"Проверено: {0}." , MyAttribute[i]->Reviewed);
      }
  }

Visual Basic.NET

Public Shared Sub GetAttribute(t As Type)
  ' Получить атрибут.
  Dim MyAttribute As DeveloperAttribute() = _
    CType(Attribute.GetCustomAttributes(t,
 GetType(DeveloperAttribute)), DeveloperAttribute())

  If MyAttribute Is Nothing Then
    Console.WriteLine("Атрибут не найден.")
  Else
    Dim i As Integer
    For i = 0 To MyAttribute.Length - 1
      ' Получить поле Имя.
      Console.WriteLine("Имя: {0}.", MyAttribute(i).Name)
      ' Получить поле Уровень.
      Console.WriteLine("Уровень: {0}.", MyAttribute(i).Level)
      ' Получить поле Проверено.
      Console.WriteLine("Проверено: {0}.", MyAttribute(i).Reviewed)
    Next i
  End If
End Sub

Получение списка разнотипных атрибутов

Методы GetCustomAttribute и GetCustomAttributes не могут искать атрибут во всем классе и возвращать все его экземпляры. Они просматривают только один метод или поле за раз. Поэтому, если есть класс с одним атрибутом для всех методов и нужно получить все экземпляры этого атрибута, не остается ничего делать, как передавать эти методы один за другим в качестве параметров GetCustomAttribute и GetCustomAttributes.

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

C#

using System;
using System.Reflection;
public static void GetAttribute(Type t)
{
  // Получить атрибут уровня класса.
  DeveloperAttribute att =
   (DeveloperAttribute) Attribute.GetCustomAttribute (t, <
   BR >
      typeof(DeveloperAttribute));

  if (att == null)
    Console.WriteLine("Класс {0} не имеет атрибута Developer.\n", t.ToString());
  else
  {
    Console.WriteLine("Атрибут Имя на уровне класса: {0}.", att.Name);
    Console.WriteLine("Атрибут Уровень на уровне класса: {0}.", att.Level);
    Console.WriteLine("Атрибут Проверено на уровне класса: {0}.\n", att.Reviewed);
  }

  // Получить атрибуты уровня методов.
  // Получить все методы данного класса и поместить их
  // в массив объектов System.Reflection.MemberInfo.
  MemberInfo[] MyMemberInfo = t.GetMethods();

  // Вывести атрибуты всех методов класса
  for (int i = 0; i < MyMemberInfo.Length; i++) {
    att = (DeveloperAttribute) Attribute.GetCustomAttribute(MyMemberInfo[i], <
    BR >   typeof (DeveloperAttribute));
    if (att == null)
      Console.WriteLine("Метод {0} не имеет атрибута Developer.\n" ,
  MyMemberInfo[i].ToString());
    else {
      Console.WriteLine("Атрибут Имя на уровне метода {0}: {1}.",
 MyMemberInfo[i].ToString(), att.Name);
      Console.WriteLine("Атрибут Уровень на уровне метода {0}: {1}.",
 MyMemberInfo[i].ToString(), att.Level);
      Console.WriteLine("Атрибут Проверено на уровне метода {0}: {1}.\n",
 MyMemberInfo[i].ToString(), att.Reviewed);
    }
  }
}

MC++

using namespace System;
using namespace System::Reflection;

public:

static void GetAttribute(Type* t)
{
  // Получить атрибут уровня класса.
  DeveloperAttribute* att =
    __try_cast<DeveloperAttribute*>(Attribute::GetCustomAttribute
 (t, __typeof(DeveloperAttribute)));

  if (att == 0)
    Console::WriteLine(S"Класс {0} не имеет атрибута Developer.\n", t->ToString());
  else
  {
    Console::WriteLine(S"Атрибут Имя на уровне класса: {0}.", att->Name);
    Console::WriteLine(S"Атрибут Уровень на уровне класса: {0}.", att->Level);
    Console::WriteLine(S"Атрибут Проверено на уровне класса: {0}.\n", att->Reviewed);
  }

  // Получить атрибуты уровня методов.
  // Получить все методы данного класса и поместить их
  // в массив объектов System.Reflection.MemberInfo.
  MemberInfo* MyMemberInfo __gc[] = t->GetMethods();

  // Вывести атрибуты всех методов класса
  for (int i = 0; i < MyMemberInfo.Length; i++) {
    att = __try_cast<DeveloperAttribute*>
(Attribute::GetCustomAttribute(MyMemberInfo[i], __typeof(DeveloperAttribute)));
    if (att == 0)
      Console::WriteLine(S"Метод {0} не имеет атрибута Developer.\n" ,
  MyMemberInfo[i]->ToString());
    else {
      Console::WriteLine(S"Атрибут Имя на уровне метода {0}: {1}.",
 MyMemberInfo[i]->ToString(), att->Name);
      Console::WriteLine(S"Атрибут Уровень на уровне метода {0}: {1}.",
 MyMemberInfo[i]->ToString(), att->Level);
      Console::WriteLine(S"Атрибут Проверено на уровне метода {0}: {1}.\n",
 MyMemberInfo[i]->ToString(), att->Reviewed);
    }
  }
}

Visual Basic.NET

Imports System
Imports System.Reflection

Public Shared Sub GetAttribute(t As Type)
  ' Получить атрибут уровня класса.
  Dim att As DeveloperAttribute = _
    CType(Attribute.GetCustomAttribute(t, GetType(DeveloperAttribute)), DeveloperAttribute)
  If att Is Nothing Then
    Console.WriteLine("Класс {0} не имеет атрибута Developer.", t.ToString())
  Else
    Console.WriteLine("Атрибут Имя на уровне класса: {0}.", att.Name)
    Console.WriteLine("Атрибут Уровень на уровне класса: {0}.", att.Level)
    Console.WriteLine("Атрибут Проверено на уровне класса: {0}.", att.Reviewed)
  End If

  ' Получить атрибуты уровня методов.
  ' Получить все методы данного класса и поместить их
  ' в массив объектов System.Reflection.MemberInfo.
  Dim MyMemberInfo As MemberInfo() = t.GetMethods()

  ' Вывести атрибуты всех методов класса
  Dim i As Integer
  For i = 0 To MyMemberInfo.Length - 1
    att = CType(Attribute.GetCustomAttribute(MyMemberInfo(i),
 GetType(DeveloperAttribute)), DeveloperAttribute)
    If att Is Nothing Then
      Console.WriteLine("Метод {0} не имеет атрибута Developer.",
 MyMemberInfo(i).ToString())
    Else
      Console.WriteLine("Атрибут Имя на уровне метода {0}: {1}.",
 MyMemberInfo(i).ToString(), att.Name)
      Console.WriteLine("Атрибут Уровень на уровне метода {0}: {1}.",
 MyMemberInfo(i).ToString(), att.Level)
      Console.WriteLine("Атрибут Проверено на уровне метода {0}: {1}.",
 MyMemberInfo(i).ToString(), att.Reviewed)
    End If
  Next i
End Sub

Для доступа к методам и полям проверяемого класса используются методы класса System::Type. В этом примере сначала через Type запрашивается информация об атрибутах, определенных на уровне класса, затем, через метод Type.GetMethods получается информация обо всех атрибутах, определенных на уровне методов. Эта информация помещается в массив объектов типа System.Reflection.MemberInfo. Если нужны атрибуты свойств, используется метод Type.GetProperties, а для конструкторов – Type.GetConstructors. Класс Type имеет множество методов для доступа к элементам типа, здесь описана только очень небольшая часть.

Пример

Демонстрационная программа, показывающая, как можно получать информацию из секций метаданных, написана на C#. На рисунке показана закладка, на которой можно увидеть значения атрибутов для сборки, на других закладках показывается аналогичная информация. Я не буду подробно описывать пример, кому надо, может скачать и скомпилировать его. Пример достаточно прост и разобраться в том, как получаются атрибуты, ни для кого не составит труда.

Заключение

Как видите, система метаданных в среде .NET достаточно хорошо продумана, и пользоваться ей легко и удобно.

Это все на сегодня. Пока!

Алекс Jenter jenter@rsdn.ru
Duisburg, 2001. Публикуемые в рассылке материалы принадлежат сайту RSDN.