Написание приложений Win32 Apps с помощью одних классов C++ (часть 1)

ОГЛАВЛЕНИЕ

В следующей статье будет рассмотрено написание приложений Win32 Apps с помощью одних классов C++

•    Скачать исходники - 29.2 Кб
•    Скачать демонстрационный проект - 39.6 Кб

Введение

При программировании на C++ для Win32 C часто применяется MFC, WTL, .NET, WxWin, или некоторый "каркас", или обертка.

Одно из достоинств в том, что они "упрощают" определённые операции, скрывая определенные детали, в то время как запутанность между каркасными классами, наоборот, затрудняет работу, если приходится делать нечто необычное.

Хотелось ли вам когда-либо при переопределении определенных классов или функции MFC взломать определенное кем-то другим поведение? Сколько раз вы обнаруживали странные побочные эффекты вследствие того, что документ, кадры и виды часто уничтожают друг друга  не всегда в очевидном порядке?

Далее  предлагается другой подход. Разумеется, нет личного состава всей команды MS AFX, поэтому не ждите полной замены WTL или ATL. Вводится лишь другая возможная точка зрения.

Будет реализована конструкция, минимизирующая запутанность классов, чтобы сделать предложенные классы многократно используемыми. В частности, если классу A нужен класс B, то классу B не нужен класс A. Следовательно, можно применять эти классы вне приведенного каркаса, возможно, даже в приложении MFC.

Использовались только контейнеры и алгоритмы STL (Standard Template Library - стандартная библиотека шаблонов) и идеи некоторых авторов CP: особенно Пола Бартлетта и Алекса Фарбера. (Их код не использовался повторно, но их идеи были истолкованы по-новому.)

Соглашения

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


Обертки долевого владения

Это ключ для всей реализации: в Win32 API полно разных описателей, приходится "получать", "использовать" и "освобождать".

Обертка не должна переделывать C API: нет особой нужды заменять ShowWindow(hWnd, SW_SHOW) на pWnd->ShowWindow(SW_SHOW): "участники" точно такие же, и нет смысла писать тысячи прототипов, чтобы вынести первый параметр за скобки.

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

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

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

Архитектура

Ядром всего механизма является взаимодействие между WrpBase и XRefCount.

WrpBase предоставляет "протокол" для обертки: ссылается на "счетчик ссылок" и может быть "прикреплен к" и "отцеплен от" обертываемого им объекта. Он также может быть назначен "владельцем" или "наблюдателем", быть "действительным" или нет, и возвращает итераторы STL для обхода коллекции оберток.

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

WrpBase является наследуемым (один из его шаблонных параметров, W, является классом, наследуемым от него) и вызывает все свои функции после приведения своего указателя - "this" к W* посредством static_cast, что позволяет переопределить его функции.

Также поведение WrpBase зависит от ряда "помощников", предоставляемых классом типажей, от которого наследует WrpBase. Еще одной задачей этого "класса типажей" является предоставление удобного числа typedef, определяющего способ, посредством которого обернутый объект хранится в обертке, получается в качестве параметра из функций-членов, задается в качестве возвращаемого значения, или разыменовывается.

Классы типажей реализуются в разных версиях и наследуют определение типа от "классов типов".
Чтобы заключить все в нечто, что может быть присвоено, скопировано и т.д., класс Wrp наследует от WrpBase или производного класса, применяя оператор присваивания, конструкторы копирования, операторы равенства (==) и упорядоченности (<), доступа (->) и разыменования (*) и т.д., с помощью определенного протокола WrpBase (после кастомизации, при наличии таковой).

Следующий рисунок иллюстрирует все вышесказанное.

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

Реализация

Типы

"types" реализуются двумя разными способами:
•    types_wrp_hnd<H>: предположим, H является описателем, или "маленьким объектом", или указателем, или ... чем-то, что можно привести к LPVOID без потери информации и что хранится "как есть".
•    types_wrp_ptr<H>: предположим, H является объектом в куче, на который ссылается интеллектуальный указатель. Следовательно, H* хранится оберткой, имеющей "семантику указателя".
Оба класса определяют следующие типы:

Тип

Описание

types_wrp_hnd

types_wrp_ptr

SH

Что обертка хранит для представления H

H

H*

IH

Что обертка будет получать в качестве входного параметра в функциях

const H&

H*

OH

Что обертка будет возвращать из своих функций

const H&

H*

RH

Что обертка будет возвращать в качестве "разыменования"

const H&

H&

В конкретных реализациях SH, IH, OH и RH могут отличаться (например: можно возвращать объект-заместитель для хранения ссылки ...).

Неявное преобразование обязательно должно работать при преобразовании IH в SH, SH в OH, и OH в IH.

Типажи

"traits" реализуются тремя разными способами:
•    traits_wrp_hnd<H, Types>: предположим, H является описателем. Большинство из его функций надо переопределить, как только H известен и его политика управления определена.
•    traits_wrp_ptr<H,Types>: предположим, H является "указываемым объектом" (обертка работает как H*) и реализует Create(создать) как new и Destroy(уничтожить) как delete(удалить). static_cast управляет преобразованиями между указателями.
•    traits_wrp_dynptr<H,Types>: то же самое, но с применением dynamic_cast.

Как правило, параметр "Types" принимает значение по умолчанию types_wrp_hnd<H> - в первом случае, и types_wrp_ptr<H> - в остальных двух случаях. Но – для некоторых особых случаев – могут предусматриваться другие решения.

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

template<class H, class Types=types_wrp_hnd<H> >
struct traits_wrp_hnd: public Types
{
    typedef Types TT;
    static bool equal(TT::IH left, TT::IH right) { return left == right; }
    static bool less(TT::IH left, TT::IH right) { return left < right; }
    static LPVOID Key(TT::IH h)  { return (LPVOID)h; }
    template<class A> static void from(const A& a, TT::SH& h) { h = (H)a; }
    static TT::IH invalid() {static TT::SH h(NULL); return h; }
    static TT::OH Access(TT::IH h) { return h; }
    static TT::RH Dereference(TT::IH h) { return h; }
    void Create(TT::SH& h) { h = invalid(); }
    void Destroy(TT::SH& h) { h = invalid(); }
};

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

WrpBase

Определяется следующим образом:

template<
    class H,    //что обертывается
    class W,    //внешняя обертка (унаследовано отсюда)
    class t_traits, //поведение недействительного H
    class t_refCount    //используемый тип счетчика ссылок
>
class WrpBase:
    public t_traits
{
public:
    typedef WrpBase TWrpBase;
    typedef W TW;
    typedef t_refCount TRefCount;
    typedef t_traits TTraits;
    typedef H TH;
    typedef TTraits::TT TT;
    friend WrpBase;
    friend TRefCount;
    friend TTraits;
protected:
    TT::SH _hnd;
    t_refCount* _pRC;
    bool _bOwn;
    ...
}

Он предоставляет хранилище для члена SH, указатель на класс t_refCount (должен иметь такой же прототип, как и XRefCount_base), и bool, определяющий, является ли данная обертка "владельцем" или "наблюдателем". Он также реализует следующее:
•    void Attach(TT::IH h) прикрепляет обертку к переданному "тупому значению". Ищутся существующие счетчики ссылок для значения и, если не найдены, создаются.
•    void AttachWrp(const WrpBase& w) прикрепляет обертку к значению, хранимому другой оберткой.
•    template<class A> void AttachOther(const A& a) прикрепляет обертку к значению, обнаруживаемому путем преобразований из типа A.
•    void Detach() отцепляет обертку. Вызывает Destroy (унаследовано от TTraits), если обертка-владелец, связанная с обернутым значением, является последней.
•    bool IsValid() const возвращает результат, если обертка и обернутый объект являются  действительными
•    bool IsOwner() const возвращает, независимо от того, является ли обертка оберткой-владельцем или нет.
•    void SetOwnership(bool bOwn) делает обертку "владельцем" (true) или "наблюдателем" (false).
•    typedef TRefCount::iterator iterator; static void get_range(iterator& first, iterator& last, TT::IH h) получает диапазон итератора из конечной коллекции оберток, связанной с обернутым значением. Если first==last –  коллекция пустая или не существует.

Они реализуются через другие функции (всегда через функцию pThis(), преобразующую this в W*) и через функции TRefCount. Он также предоставляет TRefCOunt набор ничего не делающих функций обратного вызова:
•    void OnFirstAttach(XRefCount_base* pRC) вызывается, когда счетчик ссылок считает самую первую ссылку на обернутый объект.
•    void OnAddRef(XRefCount_base* pRC) вызывается всегда, когда добавляется новая ссылка.
•    void OnRelease(XRefCount_base* pRC) вызывается всегда, когда ссылка освобождается.
•    void OnLastRelease(XRefCount_base* pRC) вызывается при последнем освобождении.

Подсчёт ссылок

Должен наследоваться от XRefCount_base, переопределяющего его функции:

class XRefCount_base
{
public:
    bool _bValid;
    XRefCount_base() { _bValid = true; }

    int owners() { return 0; }
    int observers() { return 0; }
    int reference() { return 0; }

    void AddRef(W* pW, bool bOwn)
    {}
   
    bool Release(W* pW, bool bOwn)
    {return false;}

    //не выполнять итерации
    iterator begin() { return NULL; }
    iterator end() { return NULL; }
};

Он реализован в двух версиях:

•    XRefCount<W>: W должен наследоваться от WrpBase. Реализует подсчёт ссылок владельцев и наблюдателей. (reference() – сумма двух). Он автоматически уничтожается при Release(), если ссылок больше нет, возвращая true (в противном случае false). Не предоставляет итераторы (возвращает "первый" и "последний" для случайных равных значений).
•    XRefChain<W>: хранит список W*, проталкивая вперед при AddRef и удаляя при Release. Автоматически удаляется, когда список пустеет.
Первый подходит для типов, требующих только подсчета ссылок, последний - для типов, для которых надо следить, чем являются обертки.

Обратное преобразование

XMap хранит статический std::map<LPVOID, XRefCount_base*>, предоставляющий функции Map, Unmap и Find.

Обертки знают, что они обертывают, и знают, чем является связанный счетчик ссылок. Но при присвоении значения обертке надо каким-то путем выяснить (в случае если "значение" уже обернуто чем-то другим), существует ли (и чем является) связанный XRefCount.

Классы типажей включают в себя функцию Key(IH). Она преобразует обернутый тип в LPVOID.
Это можно сделать с помощью приведения в стиле C (как у traits_wrp_hnd), static_cast (как у неполиморфных указателей в traits_wrp_ptr) и dynamic_cast (для полиморфных указателей, как в traits_wrp_dynptr).

Особенно интересно последнее: если дан сложный объект (с множеством базовых классов), разные указатели на разные компоненты могут отличаться (по абсолютному значению), но приведение посредством dynamic_cast к LPVOID всегда возвращает одинаковый базовый адрес (базовый адрес целого объекта). Это позволяет делить владение сложным объектом между разными интеллектуальными указателями, ссылающимися на разные компоненты: все они могут найти один и тот же объект подсчета ссылок.

Операторы (Wrp<B>)

Конструкторы преобразования, конструкторы копирования, операторы присваивания и т.д. не могут наследоваться как обычные функции. Их переопределение требует особого внимания, поэтому все они были собраны в специальный шаблонный класс. Wrp<B> является "классом верхнего уровня". B является классом, от которого наследуется Wrp. Это может быть любой класс, производный от WrpBase. Он реализует присваивание, копирование, преобразование и уничтожение через Detach/Attach.

Некоторые особые случаи: интеллектуальные указатели

Для специализации обертки метод может делать следующее:
1.    объявить struct, унаследованную от WrpBase, задающую требуемые параметры, затем
2.    определить тип Wrp<yourstruct> посредством typedef, чтобы обеспечить возможность присваивания и копирования.

Это происходит в простом случае определения интеллектуальных указателей:

template<class T>
struct Ptr
{
    struct StatBase:
        public WrpBase<T, StatBase,
          traits_wrp_ptr<T>, XRefCount<StatBase> >
    {};

    struct DynBase:
        public WrpBase<T, DynBase,
          traits_wrp_dynptr<T>, XRefCount<DynBase> >
    {};

    typedef Wrp<StatBase > Static;
    typedef Wrp<DynBase > Dynamic;
};

Две структуры определяют поведение для обертки указателя static_cast и dynamic_cast.

Если A является классом, интеллектуальный указатель на A может быть ...

typedef Ptr<A>::Dynamic PA;.

Интеллектуальный описатель

Таким же образом получается "интеллектуальный описатель" с помощью:

template<class H, class W>
struct HndBase:
    public WrpBase<H, W, traits_wrp_hnd<H>, XRefCount<W> >
{};

В то время как Ptrxxx struct закрывают наследование обертки (они передаются в WrpBase в качестве параметра W), HndBase не делает этого: все же имеется параметр W.

Причина в том, что, хотя для указателя new и delete однозначно являются политикой создания и уничтожения (Create и Destroy исходят из классов типажей таким образом), для описателей их все же надо определять. Разные описатели имеют разный API создания и уничтожения. Следовательно, HDC имеет многие из них! Его придется доделать позже.

Конечно, для отдельных конкретных объектов понадобится делать нечто, отличное от "new" или "delete": при этом придется самостоятельно определить struct, производную от WrpBase (и закрывающую его наследование), и затем обернуть ее в Wrp.


Связанные действия

Используя XRefChain в качестве параметра WrpBase, можно связать вместе "обертки для одной и той же сущности" и идти по ним.

С этим «хождением» можно связать действия. Например, обеспечить возможность отправлять сообщения Windows оберткам HWND, но не только.

EChainedAction<W, I, A> является наследуемым классом (W – его производный класс), связывающим действие с параметром A с проходом по итератору I, который должен быть совместимым с STL поступательным итератором, разыменовывающимся в W* (чей I::operator*() возвращает W*). Такой итератор можно получить посредством WrpBase::get_range(...).

Функция Act(const A& a, iterator first, iterator last) выполняет итерации с начала до конца, вызывая функцию-член OnAction(const A& a), предположительно переопределенную в W.

Однако итерации выполняются не путем чистого прямого цикла, а путем рекурсии по хранимым параметрам: Act заполняет структуру данных в стеке и сохраняет ее адрес в сохраненную в каждом потоке статическую переменную, храня предыдущий адрес (переменная из потока – переменная, к которой обращаются посредством карты, ключом которой является идентификатор текущего потока), затем вызывает Default(). Default извлекает структуру данных и выполняет итерации, вызывая OnAction. Если OnAction возвращает true, Default сразу возвращает true, иначе выполняет итерацию для следующего элемента, пока не достигнут конец. В этой точке возвращает false.

Следовательно, в переопределении OnAction можно:
•    Просто вернуть false. Итерация продолжится для следующего связанного элемента.
•    Просто вернуть true. Итерация остановится.
•    Вызвать OnAction при необходимости. Предполагается итерация для следующих элементов, и управление возвращается вам.

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

События

События являются сочетанием интеллектуальных указателей и связанных действий. Здесь они реализованы в виде «функторов», объявляемых как переменные-члены класса (источник события), прикрепляемые посредством внутренних объектов («приемников»), отправляющих действие зарегистрированным "получателям".

Event<A> является struct, немного превосходящей член operator()(const A&), предполагающий, что событие надлежит обернуть связываемыми обертками. Оператор получает цепь и вызывает Act.

EventRcv<D> - базовый класс для обобщенного производного класса (D), предоставляющий RegisterEvent<A>(Event<A>&,bool(D::*pmemfun)(consty A&) ) и UnregisterEvent<A>(Event<A>&). Первая функция создает XTypedSync<A,R>, связывающий событие с указанной функцией-членом. Вторая функция удаляет связь.

XTypedSync<A,R> порождается от XSync<A>, являющегося связываемой оберткой для Event, имеющей параметр A. XSync реализует OnAction, делегирующий виртуальной функции DoAction, реализованной в производном XTypedSync, вызывающем указанную функцию-член (посредством указателя на член, сохраненного во время привязки). Необходимо использовать виртуальную функцию, потому что разные XTypedSync (ссылающиеся на разных получателей, следовательно, имеющих разные параметры R) должны связывать все вместе, как XSync<A>.

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

Регистрация WNDCLASS

Если вы считаете заполнение поля такой структуры данных, как WNDCLASSEX, очень неприятной задачей, то эта обертка для вас. SWndClassExReg регистрирует класс окна, разрегистрируя его, когда больше нет ссылок.

Он порождается от WrpBase, кастомизируя Destroy.

Конструкторы пытаются заполнить WNDCLASSEX переданными параметрами:
•    Версия загружает из ресурсов с переданным идентификатором (иконка и курсор), второй конструктор использует переданные необработанные значения.
•    Оба конструктора регистрируют класс окна и сохраняют возвращенный ATOM.
•    Функция Destroy обертки (она вызывается, когда отцепляется последняя обертка-владелец) вызывает UnregisterWndClass, передавая сохраненный ATOM.

Цикл обработки сообщений (SMsgLoop)

Цикл обработки сообщений реализуется SMsgLoop. Можно создать столько его экземпляров, сколько нужно. Как правило, один будет в WinMain, тогда как остальные будут там, где нужны "модальные циклы" (например: при запросе координат мыши, чтобы определить, куда поместить объект).

Сам цикл реализуется функцией LoopBody (которая сама не является циклом), реализующей извлечение и отправку сообщений вместе с фильтрацией и переводом сообщений и обработкой во время простоя. Эта функция вызывается LoopWhile, выполняющим цикл, пока переданная ссылка bool не станет false или пока не будет получено WM_QUIT. LoopWhile вызывается Loop, предоставляющим «всегда истинное» значение для LoopWhile.

Такое "разделение цикла" на три составляющих позволяет использовать SMessageLoop для выполнения модального цикла внутри приложения (с использованием LoopWhile) или для отправки сообщения во время расчета цикла (вызывая LoopBody из главного цикла).

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

Если SMsgLoop создается с параметром bAutoQuit, установленным в true, первое окно, созданное посредством указанного цикла обработки сообщений, становится главным окном цикла обработки сообщений.


Обработка во время простоя и фильтрация сообщений

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

SIdleEvent и SMsgFilterEvent оба являются EGlobal и Event. Параметр SIdleEvent является SMsgLoop, вызывающим событие. Если вы сами создаете событие, можете передать NULL.

Параметр SMsgFilterEvent является структурой Win32 MSG. В своем обработчике события возвращайте true, чтобы указать, что переданное сообщение было отфильтровано.

Обертывание HWND: создание производных классов окон

Обертки HWND должны уметь получать сообщения окна, а значит, они должны связывать WrpBase и EChainedAction, но этого недостаточно.

Также нужно получать сообщения из системы. Это требует механизма создания производных классов/изменения классов. К тому же надо отправлять сообщения вероятно разным функциям-членам. Возможны разные подходы к этому:
•    в стиле MFC: функции-члены увязываются с адресами с соответствующими идентификаторами сообщения. Когда оконная процедура подкласса получает сообщение, она просматривает карту, ища совпадение, и затем вызывает соответствующую функцию. Можно предусмотреть набор макросов для облегчения заполнения статической карты.
•    в стиле ATL: набор макросов предоставляет фрагменты кода, необходимые для распознавания сообщений и их отправки указанным функциям. Макросы также переопределяют известную виртуальную функцию, вызываемую производным классом оконной процедуры.
•    в стиле .NET: производный класс оконной процедуры распаковывает сообщения, отправляя их набору известных членов, каждый из которых возбуждает разное событие.
Ни один из этих подходов не идеален: первые два сильно связаны с макросами, а третий требует большой структуры данных и не является симметричным: есть объект, владеющий HWND, а остальные прикрепляются к нему.

Эти изъяны компенсируются несколькими мастерами, помогающими составлять карты сообщений или связывать обработчики событий. Учитывая необходимость иметь дело с большим числом сообщений, они являются лучшим компромиссом между методиками программирования и удобством инструмента. Подход ATL/WTL более гибкий, поэтому было сделано следующее:
1.    Обертки для одного и того же HWND связаны вместе. Первое прикрепление и последнее освобождение используются для создания производного класса или изменения класса обернутого HWND.
2.    OnFirstAttach переопределяется для создания производного класса окна.
3.    Производный класс оконной процедуры вызывает Act для связанного действия, относящегося к HWND.
4.    OnAction переопределяется для вызова известной виртуальной функции с созданием такого же прототипа ATL PocessWindowMessage.
5.    Default переопределяется для вызова предыдущей оконной процедуры, если действие возвращается необработанным.
6.    Для хранения предыдущей оконной процедуры требуется место, общее для всех оберток одного и того же окна. Следовательно, приходится создавать производный класс от XRefChain для хранения этого значения.

Результат всего этого - EWnd.

Его объявление приведено ниже:

class EWnd:
    public IMessageMap,
    public NWrp::WrpBase<HWND, EWnd,NULL,traits_wrp_types<HWND>, XRefChainWnd>,
    public NWrp::EChainedAction<EWnd, EWnd::iterator, LPWndProcParams>,
    public NUtil::EAutodeleteFlag
{
public:
    void OnFirstAttach(NSmart::XRefCount_base* pRC); //создается производный класс окна
    static bool Default();  //вызывается superwndproc
    void Destroy(HWND& hWnd);  //уничтожается HWND (только владельцы уничтожаются)
    bool OnAction(const LPWndProcParams& a); //вызывается ProcessWindowMessage
   
    //copy and assign
    EWnd() {SetOwnership(false);}
    EWnd(const EWnd& w) { SetOwnership(false); AttachRefCounting(w); }
    EWnd& operator=(const EWnd& w) { AttachRefCounting(w); return *this; }
    EWnd& operator=(HWND h) { Attach(h); return *this; }
    explicit EWnd(HWND h) { SetOwnership(false);  Attach(h); }

    //реализация IMessageMap по умолчанию
    virtual bool ProcessWindowMessage(
        HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam,
        LRESULT& rResult, DWORD msgMapID) { return false; }

    //несколько вспомогательных функций, общих для всех окон
    LRESULT ForwardNotifications(UINT uMsg,
        WPARAM wParam, LPARAM lParam, BOOL& bHandled);
    LRESULT ReflectNotifications(UINT uMsg,
        WPARAM wParam, LPARAM lParam, BOOL& bHandled);
    BOOL DefaultReflectionHandler(HWND hWnd, UINT uMsg,
        WPARAM wParam, LPARAM lParam, LRESULT& lResult);

    //создание окна
    bool CreateWnd(
        HINSTANCE hInst,
        LPCTSTR clsName,
        LPCTSTR wndName,
        DWORD dwStyle,
        LPCRECT lprcPlace,
        HWND hwndParent,
        HMENU hMenu,
        LPVOID lpParams
        );
};

Примечания:
1.    IMessageMap – интерфейс, объявляющий функцию ProcessWindowMessage в стиле ATL.
2.    порождается от ERefChaining, но использует XRefChainWnd. Порождается от XRefChain, но также хранит указатель функции WNDPROC и счетчик рекурсий.
3.    порождается от EChainedAction, используя LPWndProcParams. XWndProcParams является struct, принимающей hWnd, uMsg,wParam, lParam и lResult.
4.    порождается от EAutoDeleteFlag. Если установлен в true (значение по умолчанию - false), то обертка удаляется при уничтожении окна
5.    обертка всегда создается как "невладелец" (наблюдатель). Назначение ее "владельцем" уничтожит HWND, если все владельцы отцеплены.

Бесполезная программа

Очень простая, бесполезная, но показывает все вышеописанное в действии.

#include "stdafx.h"

#include "NLib/MsgLoop.h"
#include "NLib/Wnd.h"

using namespace GE_;

int APIENTRY _tWinMain(HINSTANCE hInstance,
    HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
    NWin::SMsgLoop loop(true);  //создается экземпляр цикла
   
    LPCTSTR wcname = _T("W1MainWnd");
    NWin::SWndClassExReg wc(true, hInstance, wcname);
    //создается экземпляр класса окна
   
    NWin::EWnd wnd;
    wnd.CreateWnd(hInstance, wcname, NULL,
        WS_VISIBLE|WS_OVERLAPPEDWINDOW,
        NWin::SRect(NWin::SPoint(CW_USEDEFAULT,CW_USEDEFAULT),
        NWin::SSize(600,400)), NULL, NULL, NULL);

    loop.Loop();

    return 0;
}

Создается окно на базе DefWndProc (передается SWndClassExReg в качестве параметра по умолчанию), не делающее ничего в исполнимом файле размером 76Кб.

Не самый маленький exe, но примите во внимание все функции типажей, std::map(карты), list(списки), и т.д. Они повышают гибкость, но имеют издержки!


Hello World: обертки GDI

Для начала надо создать EWnd в SWnd и реализовать карту сообщений, отправляющую WM_PANT.
Затем нужен макрос карты сообщений, далее нужны избранные обертки объекта GDI, или надо включить библиотеку GDI+.

Макрос карты сообщений

В духе макросов ATL они имеют такие же имена, чтобы позволить мастеру IDE также делать свою тяжелую работу, но они не идентичны ATL (они рассчитаны на работу в обертке EWnd, а не CWnd или CWindow!). Они включены в MessageMap.h. Простейшие макросы ATL тоже работают хорошо, но не макросы WTL.

Макросы распаковщика сообщений имеют такие же имена и параметры, что и у WTL, но иную реализацию.

Примечание: Макросы были получены путем интенсивного использования "поиска и замены" из первоисточников. Они являются сотыми, поэтому не были все протестированы единолично, но поскольку процесс был одинаковым для всех и так как протестированные макросы работали, было предположено, что все они работают.

Но не забывайте тестировать макросы по мере использования.

Ниже приведена очень простая программа "hello world":

// W2.cpp: тестовое приложение W32

#include "stdafx.h"

#include "NLib/MsgLoop.h"
#include "NLib/Wnd.h"

using namespace GE_;

class CMainWnd: public NWin::EWnd
{
public:
    BEGIN_MSG_MAP(CMainWnd)
        GETMSG_WM_PAINT(OnPaint)
    END_MSG_MAP()
    void OnPaint();
};


int APIENTRY _tWinMain(HINSTANCE hInstance,
  HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
    NWin::SMsgLoop loop(true);  //создается экземпляр цикла
   
    LPCTSTR wcname = _T("W1MainWnd");
    NWin::SWndClassExReg wc(true, hInstance, wcname);
    //создается экземпляр класса окна
   
    CMainWnd wnd;
    wnd.CreateWnd(hInstance, wcname, NULL,
        WS_VISIBLE|WS_OVERLAPPEDWINDOW,
        NWin::SRect(NWin::SPoint(CW_USEDEFAULT,CW_USEDEFAULT),
        NWin::SSize(600,400)), NULL, NULL, NULL);

    loop.Loop();

    return 0;
}

void CMainWnd::OnPaint()
{
    PAINTSTRUCT ps;
    BeginPaint(*this, &ps);
   
    NWin::SRect R;
    GetClientRect(*this, &R);
    DrawText(ps.hdc, _T("Hallo World !"),-1,
          R, DT_CENTER|DT_VCENTER|DT_SINGLELINE);

    EndPaint(Value(), &ps);
}

Разве код не хорош? Еще нет: что такое пары BeginPaint - EndPaint? Почему бы не обернуть HDC в обертку, правильно управляющую парами «начало-конец» или «получить-освободить»? И как насчет сохранения и восстановления состояния перед выходом?

Обертывание HDC(описатель контекста устройства)

Идея сохранить состояние DC(контекст устройства) при обертывании кажется хорошей, но где должно сохраниться состояние?

Есть два возможных ответа:
•    В самой обертке: каждая обертка одного и того же DC хранит свое собственное состояние, сохраняя его при Attach и восстанавливая его при Detach. Это полезно для временной обертки, прикрепляемой в начале функции, уничтожение которой в конце восстановит DC и закроет его. Вызовы вложенных функций создают серию оберток, уничтожаемых в обратном порядке, что приемлемо.
•    В объекте счетчика ссылок: сохраненное состояние совместно используется всеми обертками одного и того же HDC и восстанавливается при отцеплении последнего владельца. Это хорошо для долговечных оберток, передаваемых в виде значений между функциями или сохраняющих обернутый объект, пока другие объекты ссылаются на него. Это имеет место для "SuperWndProc" в EWnd, но не так для HDC: его нормальный срок службы не выдерживает обработку команды.

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

Все классы основаны на SHdcSave_base<W>, переопределяющем OnAddRef и OnRelease для сохранения и восстановления состояния DC. Затем:
•    SHdcSave(HDC): закрытие SHdcSave_base, с конструктором, принимающим HDC в виде аргумента.
•    SHdcPaint(HWND): переопределяет Create, вызывая BeginPaint, и Destroy, вызывая EndPaint. Конструктор хранит HWND. PAINSTRUCT доступен как read_only.
•    SHdcClient(HWND): Create вызывает GetDC, а Destroy вызывает ReleaseDC.
•    SHdcWindow(HWND): Create вызывает GetWindowDC, а Destroy вызывает ReleaseDC.
•    SHdcMem(SIZE): создает контекст устройства памяти, совместимый с экраном, и хранит совместимый битовый массив с выбранным в него заданным SIZE. Годится для двойной буферизации.
С SHdcPaint метод OnPaint становится таким:

void CMainWnd::OnPaint()
{
    NGDI::SHdcPaint dc(*this);

    NWin::SRect R;
    GetClientRect(*this, R);
    DrawText(dc, _T("Hallo World !"),-1,
         R, DT_CENTER|DT_VCENTER|DT_SINGLELINE);
}

HPEN, HBRUSH, HFONT 

Объекты GDI(интерфейс графического устройства) могут создаваться разными способами, но всегда уничтожаются с помощью одного и того же API: DeleteObject.

Следовательно, SGdiObj<H> обертывает любой HGDIOBJECT посредством HndBase, переопределяя обратный вызов Destroy путем вызова DeleteObject.

Важно, что объекты GDI уничтожаются, когда объекты не выбраны в некоторый HDC. Это достигается путем инициализации оберток GDI до инициализации обертки HDC. Такой прием позволит обертке HDC уничтожиться перед восстановлением своего состояния и освободить объект GDI, который уничтожат его собственные обертки.

Ниже приведен метод "OnPaint" с другим шрифтом:

void CMainWnd::OnPaint()
{
    NWin::SRect R;
    GetClientRect(*this, R);

    NGDI::TFont font(CreateFont(60,0,0,0, FW_BOLD,
        0,0,0, 0,0,0,ANTIALIASED_QUALITY, FF_SWISS, NULL));
   
    NGDI::SHdcPaint dc(*this);
    SetTextColor(dc, GetSysColor(COLOR_WINDOWTEXT));
    SetBkColor(dc, GetSysColor(COLOR_WINDOW));
    SelectObject(dc, font);

    DrawText(dc, _T("Hello World !"),-1,
        R, DT_CENTER|DT_VCENTER|DT_SINGLELINE);
}

Заметьте, что font создается раньше dc, значит dc будет уничтожен первым. Когда это происходит, вызываются "RestoreDC и затем EndPaint". RestoreDC отменяет выбор шрифта, EndPaint закрывает рисование. В этот момент уничтожение font удаляет объект GDI.