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

ОГЛАВЛЕНИЕ

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

• Скачать проект и демо - 97.7 Кб

Введение

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

Во-первых, чтобы исправить ряд ошибок и изъянов проектирования; во-вторых, для повышения удобства и гибкости кода.

Перед началом

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

Данная статья предполагает знание вопросов, рассмотренных в части 2.

Исправленные ошибки

Ниже приведены исправленные ошибки из предыдущей версии.

"Сбой Нормана Бейтса"

Благодарим Нормана Бейтса за обнаружение ошибки, при анализе оказавшейся ошибкой проектирования. WrpBase::~WrpBase изначально вызывал pThis->Detach(). Это неверно по двум причинам:

1.pThis() приводит WrpBase к производному типу W, но внутри деструктора WrpBase W уже уничтожен!

2.Detach вызывает объект счетчика ссылок функции Relese, который может повторно вызвать объект W (уже уничтоженный).

От этого нельзя избавиться: автоматическое отцепление оберток является их мощью, но создает трудности. Решение - вызывать Detach() из каждого деструктора производного класса WrpBase. Чтобы не забыть об этом, ASSERT был помещен внутрь деструктора WrpBase: в тот момент не должно существовать никаких _pRC!

Это эквивалентно вызову "DestroyWindow" в производном деструкторе MFC CWnd. Другие неприятные ошибки, касающиеся безопасности типов, прятались в Detach и XRefCount_base. Но они были полностью устранены в переработанной новой конструкции. Но напоследок еще одна ошибка.
SShare<T> и SString

Вообще SString работает, но в его родителе есть дефект:

SShare::GetBuffer реализован через _OwnBuffer, но _OwnBuffer ошибочно был реализован через lstrcpyn. Это делало SShare подходящим только для TCHAR. Реализация была переделана на основе XBuffData::copyfrom, копирующего данные с помощью " ="и сравнивающего с помощью " ==". Сейчас она подходит для всех классов, являющихся присваиваемыми и сравниваемыми на равенство, и имеет "пустое значение".

Конечно, SString остается SShare<TCHAR, _T('\0')>. Но сейчас даже можно иметь SShare<double,0.>, or SSHrae<SSomeStruct, SSomestruct()> при необходимости.

Разделение библиотеки

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

Поэтому было решено увеличить число модулей, разделив библиотеку: NLib будет лишь "корневой библиотекой". Все возможности войдут в отдельные библиотеки, сделав корневую библиотеку (ядро) открытой для разных улучшений.

Корневая библиотека NLIB

Какие модули войдут в корневую библиотеку?

inside stdafx.h

 

определения GE_INLINE иGE_ONCE

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

Глобальные идентификаторы

определение ключевого слова interface (должно браться изwindows.h, но ...),crtdbg.h, определения ASSERT иVERIFY

коллекциииалгоритмыSTL

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

заголовки Windows и частые заголовки

windows.h, commctrl.h, olectrl.h, tchar.h

заголовкиядраNlib

Wrp.h, Wnd.h, WinString.h, MessageMap.h, ... и их загруженные заголовки:Misc.h,Coords.h windowlongptr.h) иEvt.h

outside stdafx.h

 

обертки GDI

GdiWrp.cpp иGdiWrp.h

Начинка цикла обработки сообщений

Msgloop.h иMsgloop.cpp

Загрузчик ресурсов

ResWrp.h

Обертки файловисериализация

Пока отсутствует в библиотеке (скоро выйдет).

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

Библиотека NGUI

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

Фактически она содержит CmdUpdt.cpp, CmdImgs.cpp и связанное описание ресурсов, включаемых в описание ресурсов приложения (Ngui.rc и NGui_res.h), а также все развертывание этого этапа.

Соглашения о нумерации для описаний ресурсов

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

Учитывая то, как Windows использует коды сообщений, коды команд и т.д., лучше избегать идентификаторов меньше 0x2000 (WM_USER + OCM__BASE), которые могут быть округлены до десятичного 8200.

Следовательно, NLIB_FIRST равен 8200, а NGUI_FIRST равен 8300. Такие числа подходят для контрольных идентификаторов. Для команд желательно оставаться в высшей части WORD (от 32768 и выше) во избежание перепутывания команд с контрольными уведомлениями. Значит, команды могут иметь то же предыдущее соглашение о нумерации, но с прибавлением смещения десятичного 40000.

Итак, 83xx будет ресурсами NGUI, а 483xx будет командами NGUI. 32768 (0x8000) также является значением по умолчанию, используемым EMenuUptator для принятия решения, отправлять или нет свои сообщения-запросы. Команды со значениями меньше 32768 не будут обновляться автоматически.


Переделанные возможности

Самая первая тяжелая переделка касается Wrp.h. Ядро было переделано для улучшения производительности и работы.

Модификация карт оберток

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

У вышеназванного есть два главных недостатка.

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

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

Специализация карт

Специализация карт предотвращает вышеназванное. Чтобы сделать это:

•  Структуры "types_wrp_xxx" struct определяют новый typedef по имени TKey, имеющий менее обобщенное значение, чем LPVOID. Это позволяет иметь разные EMap, специфичные для разных types_wrp. TKey обычно является псевдонимом для типа H, но может отличаться в отдельных случаях.
•  Функция traits_wrp_xxx::Key сейчас возвращает TT::TKey.
•  Функции Attach и Detach в WrpBase были изменены в соответствии с новыми типами: TT::TKey вместо LPVOID.

В частности:

types_wrp_hnd<H>typedef typename H TKey;
types_wrp_ptr<H>typedef typename LPVOID TKey;
types_wrp_rsrc<Data>typedef typename HRSRC TKey;

Из-за этих определений описатели имеют одну карту на каждый тип (одну для каждого H), тогда как указатели имеют одну глобальную карту (из LPVOID). Это обеспечивает работу полиморфизма (указатели разных типов могут указывать на разные компоненты –базовые классы - одного и того же сложного объекта: он должен иметь одну идентичность).

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

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

Для этого служит EMap_base вместе с EMap<> и EMap<>::TMap и скрытым XInit. Также в отладочном режиме была добавлена статистическая характеристика для карт, показываемых при завершении программы.

PtrCreator

NWrp::Ptr не дает сбой при непостоянном разыменовании NULL. В этом случае он вызывает "функцию автоматического создания", передаваемую как шаблонный параметр (если NULL, предполагается статическая функция, делающая "new T"). SetAutoCreateFn может быть вызвана при выполнении для изменения функции создания (например, для передачи функции, делающей "new" для типа, производного от T).

Поскольку такой функционал обычно нужен для динамических и полиморфных типов, он существует только в виде NWrp::PtrCreator<class, ctretorfunction>::Dynamic.

Безопасность типов

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

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

XRefChain (теперь названный SRefChain<W>) унаследован от XRefChain_base<W,D>, который, в свою очередь, унаследован от XRefCount_base<D>.  W является оберткой, для которой предназначен счетчик ссылок или цепь, а D является конечным производным классом самого счетчика ссылок (который W дает в качестве своего TRefCount).

WrpBase<H,W,t_trais,t_refCount> теперь имеет структуру SRefCount struct в качестве  значения по умолчанию для t_refCount. Образующие цепь обертки унаследованы от WrpBase (ранее они были тем же самым) как WrpChainBase<H,W,t_traits,t_refCount> и имеют дополнительные функции для получения итераторов через цепь оберток. Ожидается, что они имеют SRefChain или производный счетчик ссылок и соединитель (t_refCount принимает значение по умолчанию SRefChain<W>).

Поскольку счетчик ссылок и обертка теперь знают о своих соответствующих классах, все функции и обратные вызовы теперь типобезопасны (приведение типов не требуется).

Владеющие интеллектуальные указатели сейчас получаются как Ptr<Type>::Static или Ptr<Type>::Dynaimc (первый преобразует типы с помощью static_cast, второй – с помощью dynamic_cast). Наблюдающие указатели - Qtr<Typr>::Static и::Dynamic. Любую обертку можно превратить из наблюдателя во владельца путем вызова функции-члена SetOwnership. Ptr и Qtr – всего лишь сокращения с разным поведением по умолчанию, но, по сути, равноценны по возможностям.

EAutodeleteFlag

Этот класс изначально является источником для логического флага и функцией для его извлечения. Он используется в EWnd, где "автоматическое удаление" реализовано посредством "delete this", ссылающегося на обертку, наблюдающую WM_NCDESTROY (последнее сообщение, наблюдаемое окном в своей жизни). Это хорошо для оберток окна, существующих в куче и принадлежащих обернутому ими окну.

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

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

Во избежание этого EAutodeleteFlag соединяются в статический список при установке "включено" и удаляются при установке "выключено". Уничтожение списка (при завершении программы) удаляет все существующие объекты.

Хитрость показана ниже:

class EAutodeleteFlag
{
protected:
    bool _bHasAutodelete;
    struct XChain: public std::list<EAutodeleteFlag*>
    {
        ~XChain()
        { ... }
    };
    static XChain& AutoDeleteChain() { static XChain c; return c; }
public:
    EAutodeleteFlag() { _bHasAutodelete = false; }
    virtual ~EAutodeleteFlag() { AutoDeleteChain().remove(this); }
    bool HasAutodelete() const { return _bHasAutodelete; }
    void Autodelete(bool bOn)
    {
        if(bOn && !_bHasAutodelete) AutoDeleteChain().push_back(this);
        if(!bOn && _bHasAutodelete) AutoDeleteChain().remove(this);
        _bHasAutodelete = bOn;
    }
};

SRange<I> и SLimit

Добавлены bool IsEmpty() и bool IsUnit() с очевидным смыслом. compare теперь имеет псевдоним oparator&, когда один из операндов имеет тип I (шаблонный параметр).

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

struct SLimit
{
    template<class A> static A Min(const A& left, const A& right)
    {return (left<right)? left:right; }
    template<class A> static A Max(const A& left, const A& right)
    {return (right<left)? left:right; }
    template<class A> static A& OrMin(A& ref, const A& val)
    { if(val<ref) ref=val; return ref; }
    template<class A> static A& OrMax(A& ref, const A& val)
    { if(ref<val) ref=val; return ref; }
    template<class A> static A
      OrRange(A& rmin, A& rmax, const A& val)
    { OrMin(rmin, val); OrMax(rmax, val); return val; }
    template<class A> static A&
      AndRange(A& ref, const A& min, const A& max)
    { OrMax(ref, min); OrMin(ref, max); return ref; }
};

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

Во избежание неправильного смешения между ATL, WTL, MFC и макросами было решено снабдить их всех префиксом GE_. Все пространства имен тоже получают префикс GE_, поэтому если он не подходит вам, проведите глобальный поиск и замену во всех файлах! Нет риска путаницы с макросами с такими же именами, делающими почти то же самое, но не обязательно идентичными, особенно в проектах со смешанной средой.

Все они находятся в MessageMap.h, куда были добавлены дополнительные макросы, специализирующие разновидности WM_PARENTNOIFY.

Пересылка, отражение и автоматическое обновление команд

В предыдущей статье был введен способ управления обновлениями команд на базе NWin::ICmdState::SendQueryNoHandler и NWin::ICmdState::SendQueryUpdate.

Эти функции отправляли два закрытых сообщения GE_QUERYCOMMANDHANDLER и GE_UPDATECOMMANDUI. Так как эти сообщения связаны с командами, правильно обращаться с ними как с командами при пересылке или отражении уведомлений.

Чтобы не изменять поведение EWnd всегда, когда нужно новое уведомление, было решено переделать эту возможность (и реализовать будущие возможности на базе уведомительных сообщений) не на основе сообщения WM_USER+xxx, а на основе нового закрытого WM_NOTIFY (чтобы его можно было переслать или отразить).

Кстати, для регистрации кода уведомления использовался SNmHdr (смотрите далее). ICmdState был перемещен в NGDI и сокращен для обработки состояния команды (активно, недоступно, выбрано и текст).

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

Такие интерфейсы связаны с абстрактными структурами, порождаемыми от самих интерфейсов и от NUtil::SNmHdr<> (смотрите далее). Это позволяет отправлять уведомительные сообщения, несущие эти интерфейсы (структуры struct являются SCmdStateNotify и SCmdImageNotify).

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

Эти делается при поддержке EMenuUpdator и EMenuImager, отправляющих те сообщения. Макросы для обработки команд были изменены в соответствии с новой реализацией (серия GE_COMMAND_xx_HANDLER_U), тогда как макросы серии GE_UPDATECOMMANDUI были удалены. Обновление команды можно подключить с помощью нового GE_NOTIFY_xx_REGISTREDHANDLER(..., func, type) с использованием SCmdXxxxNotify в качестве типа при необходимости.

EMenuImager, SCmdImages

Эти классы были переделаны, чтобы сделать рисунки настраиваемыми. В частности, SCmdImgs теперь абстрактный, и SCmdImgsIDE реализует его путем обработки и прорисовки изображений команд. Можно реализовать другие SYourCmdImgs, иначе обрабатывая и рисуя эти изображения.

SCmdDraw использует новый интеллектуальный указатель NWrp::PtrCreator. Этот указатель вообще не дает сбой при разыменовании, вызывая (если NULL) заданную функцию создания. В случае SCmdDraw имеется typdef PtrCreator <SCmdImgs, SCmdImgsIDE::New> PCmdImgs, где "New" является статической функцией, возвращающей new SCmdImgsIDE.

Создание PCmdImgs извлекается с помощью функции static SCmdImgs& GetCmdImgs(). Другая функция (SetCmdImgsType(SCmdImgs* (*pfn)()) очищает PCmdImgs и присваивает его функции создания переданное значение. При следующем разыменовании указателя будет создан новый производный SCmdImgs.

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

Продолжение следует в четвертой части набора статей по данной теме.