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

Выпуск 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.

Предыдущие выпуски     Статистика рассылки