Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Writing Win32 Apps with C++ only classes (part 3)

0.00/5 (No votes)
20 Jun 2004 1  
C++ classes and wrappers to write W32 apps without MFC, ATL or other (part 3).

Introduction

OK, here I'm, back again to tell about another part of this story.

For the GUIs (oops ... guys) who are interested, the previous parts are here:

And since we are at the starting point ... let me confess that this was not originally intended to be the third part of the main article. Why? Because, while developing the docking framework, I've come to the conclusion that - having the code reached a certain maturity - a general revamping should be necessary.

First, to fix some bugs and design flaws; second, to improve code usability and flexibility. So, I started the "part 2 1/2" with the idea to title the next "part 33 1/3". But somebody else already did it, so I come back to a traditional numbering.

Before starting

This article treats the reasons for the evolution of the code from the previous to this new implementation. Although the general philosophy is the same, there are certain substantial differences. If you are interested in this implementation, just download the code provided with this article. If you are interested in the overall discussion, download also the code of the previous article: it's out of date, but it is the base to compare.

This article assumes you are already familiar with what was discussed in part 2.

Fixed bugs

Here are some of the fixed bugs of the previous version.

The "Norman Bates Crash"

As already stated in the previous article message board, I must very much thank Norman Bates, for discovering a bug that - on analyzing - I discovered to be a "design bug".

WrpBase::~WrpBase was originally calling pThis->Detach(). This is wrong for two reasons:

  1. pThis() casts WrpBase to the derived W type, but, inside WrpBase destructor ... W is already destroyed!
  2. Detach calls the reference counter object Relese function, that may call-back the W object (still already destroyed).

There is apparently no way to get out of this: auto-detaching of wrappers is the strength of wrappers, but ... that's tricky.

The solution is ... call Detach() from every WrpBase derived class destructor. And to avoid forgetting some, I placed an ASSERT inside the WrpBase destructor: no _pRC must yet exist at that time!

This is the equivalent in calling "DestroyWindow" in an MFC CWnd derived destructor.

Other nasty bugs were hidden in Detach and XRefCount_base, related to type safety. But, because I restructured, they have been completely eliminated by the new design. But let me first finish out the bugs.

SShare<T> and SString

Well, in fact SString works, but I did a flaw in its parent:

SShare::GetBuffer is implemented via _OwnBuffer, but _OwnBuffer was erroneously implemented in terms of lstrcpyn. This was making SShare good only for TCHARs. The implementation had been rewritten in terms of XBuffData::copyfrom, that copies the data using " ="and compares using " ==". It is now suitable for every class that is assignable and comparable for equality, and has a "nullvalue".

Of course, SString continues to be a SShare<TCHAR, _T('\0')>. But now, you may even have SShare<double, 0.>, or SSHrae<SSomeStruct, SSomestruct()> if it may have sense!

Splitting up the library

OK, finished with holding annoying bugs, let's go further.

While working to add features, I had a bad feeling by looking at the Solutions Explorer and opening the class view over the NWin namespace: they're risking to become monsters.

For this reason, I decided to introduce a more modular conceptualization, by splitting the library: NLib will be only the "core library". All the "features" will go into separated libraries, letting the core to be open to different improvements.

The NLIB core library

So: what will be the designated module to be part of the "core"?

inside stdafx.h
 
GE_INLINE and GE_ONCE definitions Used to obtain code that can compile either as library or as "inlined"
Global symbols interface keyword definition (should come from windows.h, but ...), crtdbg.h, ASSERT and VERIFY definitions

STL collections and algorithms

exception, functional, utility, vector, list, set, map, deque, stack, queue, bitset.
Windows headers and frequent headers windows.h, commctrl.h, olectrl.h, tchar.h
The Nlib "core" headers Wrp.h, Wnd.h, WinString.h, MessageMap.h, ... and their loaded headers: Misc.h, Coords.h (and windowlongptr.h) and Evt.h
outside stdafx.h
 
GDI wrappers GdiWrp.cpp and GdiWrp.h
Message loop stuffs Msgloop.h and Msgloop.cpp
Resource loader ResWrp.h
File wrappers and serialization Not yet deployed in this library (coming soon).

All of the modules in NLIB will change their interfaces in future, only to add new functions or members. No existent function prototypes should be modified anymore.

The NGUI library

Will contain all modules that use NLib to implement GUI other features, like owner drawn menu, docking etc.

It actually contains CmdUpdt.cpp, CmdImgs.cpp, and associated resource script to include in the application resource script (Ngui.rc and NGui_res.h), plus all the deployment of this stage.

Numbering conventions for resource scripts

When using different modules, a common convention must be established about the usage of IDs in resource files.

Considering the use that Windows does with message codes, command codes etc., it is probably better to avoid ID lower than 0x2000 (WM_USER + OCM__BASE), that can be rounded to decimal 8200.

Thus, NLIB_FIRST is 8200 and NGUI_FIRST is 8300. These numbers can be good for control IDs.

For commands, it is preferable to stay in the higher part of a WORD (from 32768 and up) to avoid to confuse commands with control notifications. Hence, commands can have the same previous numbering convention, but adding an offset of decimal 40000.

Thus, 83xx will be NGUI resources, and 483xx will be NGUI commands.

Note that 32768 (0x8000) is also the default value used by EMenuUptator to decide if sending or not its query messages. Commands having values less that 32768 will not be subject to autoupdating.

Redesigned features

The very first heavy redesign is about Wrp.h. Yes, the core has been restyled to allow better performance and operations.

Wrapper maps re-engineering

In the previous versions, all wrappers inherit a traits function that casts to LPVOID the wrapped type. This value is used as a key in a static global reverse map (XMap) that allows to find the XRefCount_base associated to the wrapped value (usually, a Windows handle or a pointer).

There are two main drawbacks in doing this.

First: consider an application with hundred windows, and a "document" (the application data) consisting in a collection of thousand polymorphic objects sustained by smart pointers (that are wrappers of pointers!). As a consequence, the reverse map becomes huge (and hence, slow) and visited very frequently (practically every time Windows sends a message).

Second: Consider an object that is wrapped by wrappers of different types using different refcounting types (for example, to store different kinds of information). If the same instance of that object is wrapped by more than one wrapper of different types, because only one reference count can exist, some wrappers will not find the required data. This is a problem, because there is no "type safety".

Maps specialization

This can be avoided by specializing the maps.

To do this:

  • The "types_wrp_xxx" structs define a new typedef, named TKey, and having a less generic value than LPVOID. This allows to have different EMaps specific for different types_wrps. TKey is usually an alias for type H, but can be different in particular cases.
  • The traits_wrp_xxx::Key function now returns TT::TKey.
  • The Attach and Detach functions in WrpBase have been modified accordingly to the new types: TT::TKey instead of LPVOID.

In particular:

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

Because of these definitions, handles have one map per type (one for every H), while pointers have a single global map (of LPVOIDs). This is required to let polymorphism to work (pointers of different types may point to different components -bases- of the same complex object: it must have a single identity).

Another problem that must be solved is the fact that those maps, being declared inside static functions, are created the first time they are needed. But, if what needs them is a global wrapper, or pointer, or event, this makes their construction to happen after the constructor of the global "needer". And, as a consequence, their destruction will happen before, causing memory corruption.

To avoid this, their destruction must be delayed as late as possible. This is accomplished by creating the maps on the heap, and letting them chain themselves into a global list that is instantiated globally and as soon as possible, and that destroys the contents on deletion.

This is what EMap_base is for, together with EMap<> and EMap<>::TMap and the hidden XInit.

Also, in debug mode, I added some statistic feature about maps, that are traced out on program termination.

PtrCreator

This is a NWrp::Ptr that doesn't fail in case of NULL non-const dereference. In that case, it calls a "autocreate function", passed as a template parameter (if NULL, a static function doing "new T" is assumed). The SetAutoCreateFn can be called runtime to change the creation function (for example, to pass a function that does a "new" for a type derived from T).

Because this kind of functionality is normally required when dynamic and polymorphic types are required, it exists only in the form NWrp::PtrCreator<class, ctretorfunction>::Dynamic.

Type safety

To grant a better type safety, the relation between wrapper, refcounters and maps has been reviewed.

XRefCount is now implemented through a common base all the reference counters inherit from. And is consistently named SRefCount. But the "owners counter" (that is, the counter that defines the wrapped object lifetime) is no more in the SRefCount itself, but is referred from a static map using the same TKey of the wrapper the SRefCount-er is for.

This allows to have refcounter of different classes counting together over a same value associated to the same keyed object, even maintaining different global maps.

XRefChain (now named SRefChain<W>) derives from XRefChain_base<W,D> that, in turn, is derived from XRefCount_base<D>.

W is the wrapper the reference counter or chain is for, and D is the ultimate derived class of the reference counter itself (what W expects as its TRefCount).

WrpBase<H,W,t_trais,t_refCount> now has as a default for t_refCount the SRefCount struct.

Chainable wrappers are derived from WrpBase (previously, they where the same thing) as WrpChainBase<H,W,t_traits,t_refCount>, and have some more functions to get the iterators across the wrapper chain. They are expected to have a SRefChain or derived reference counter and chainer (t_refCount defaults to SRefChain<W>).

Because the reference count and the wrapper are now aware of their respective classes, all functions and callbacks are now type safe (no type casting is necessary).

Owning smart pointers can now be obtained as Ptr<Type>::Static or Ptr<Type>::Dynaimc (the first converts types with static_cast, the second with dynamic_cast). Observing pointers are Qtr<Typr>::Static and ::Dynamic also. Note that every wrapper can be changed from observing to owner by calling the SetOwnership member function. Ptr and Qtr are just shortcuts with different default behavior, but substantially equivalent in their capabilities.

EAutodeleteFlag

This class is originally a provider for a boolean flag and a function to retrieve it. It is used in EWnd, where "autodeletion" is implemented with a "delete this" referred to a wrapper that's seeing a WM_NCDESTROY (the last message a window sees in its life). That's good for window wrappers existing on heap and owned by the window they wrap.

But there is a potential problem in this: suppose your program instantiates a modeless un-owned tool window: it's a popup-window, so it is not a child of your main window. Now, suppose your main window is being closed. All children are destroyed, and a WM_QUIT is posted (it's automatic if the window has an EWnd wrapper around). After all messages are gone, the message loop exits. But the popup is still there: no-one has removed it.

For the operating system, that's not a problem (it will do it after the WinMain returns), but no message dispatching is still in place. So, no WM_NCDESTROY is processed by the popup wrapper that survives the program termination (and - in fact - it is a leak!).

To avoid this, now EAutodeleteFlags chain together into a static list when set "on" and remove when set "off". The list destruction (at program termination) does a deletion of all objects still in place.

Here's the trick:

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> and SLimit

Just added bool IsEmpty() and bool IsUnit(), with obvious meaning. Also, compare is now aliased with oparator& when one of the operands is of type I (the template parameter).

SLimit, is - instead - an empty class with all static template member functions (just a way to group what I didn't want to become "global") performing some frequently "compare and assign" operations, like "let a value not to go over a given maximum" etc.

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; }
};

Message map handlers

To avoid improper mixing between ATL, WTL, MFC, and my macros, I decided to give all them a GE_ prefix. Of course, all namespaces become with GE_ also, so if those initials are not suitable for you ... a global find and replace to all files, and that's it! No risk of improper confusion with macros with same names, doing almost the same things, but not necessarily identical. Especially in mixed environment projects.

They're all in MessageMap.h, where I also added some more macro specializing WM_PARENTNOIFY flavors.

Command forwarding / reflection, and autoupdating

In the previous article, I introduced a way to manage command updates, based on NWin::ICmdState::SendQueryNoHandler and NWin::ICmdState::SendQueryUpdate.

These functions send the two private messages GE_QUERYCOMMANDHANDLER and GE_UPDATECOMMANDUI. Now, since those messages are related to commands, it is correct to treat them with the same logic of commands when subjected to notification forwarding or reflection.

To avoid to modify EWnd behavior every time a new notification is required, I decided to re-implement this feature (and to implement future features based on notification messages) not in terms of WM_USER+xxx message, but in terms of new private WM_NOTIFY (so that it can be forwarded or reflected).

And, by the way, I used SNmHdr (see later) to register the notification code.

ICmdState has been moved in NGDI, and reduced to handle the state of a command (Enable, Gray, Checked and Text).

Images are loaded from bitmaps and stored with various effects in SCmdImgs, and a new interface ICmdImage has been defined for handling setting and retrieving of image association to commands.

Such interfaces are associated to abstract structures that derive from both the interfaces themselves and from NUtil::SNmHdr<> (see later). This makes us able to send notification messages carrying those interfaces (the structs are SCmdStateNotify and SCmdImageNotify).

By deriving those interfaces, it is possible to implement the virtual functions specifically for the various kinds of interfaces (menu, toolbar, statusbar or whatever).

This is done in supporting EMenuUpdator and EMenuImager, now sending those messages.

The macros to handle commands have been modified according to the new implementation (GE_COMMAND_xx_HANDLER_U series), while the macros GE_UPDATECOMMANDUI series have been removed. Command updating can be hooked using the new GE_NOTIFY_xx_REGISTREDHANDLER(..., func, type) using - as type - the SCmdXxxxNotify as required.

EMenuImager, SCmdImages

A rearrangement of these classes has been done to make the drawings customizable.

In particular, SCmdImgs is now abstract, and SCmdImgsIDE implements it by handling and drawing command images. You can now implement yourself other SYourCmdImgs, handling and drawing those images differently.

SCmdDraw makes use of a new NWrp::PtrCreator smart pointer. This pointer is designed to never fail in dereference, by calling (if NULL) a given "creation" function. In the case of SCmdDraw, we have typdef PtrCreator <SCmdImgs, SCmdImgsIDE::New> PCmdImgs, where "New" is a static function returning new SCmdImgsIDE.

The PCmdImgs "creature" can be retrieved with the static SCmdImgs& GetCmdImgs() function. Another function (SetCmdImgsType(SCmdImgs* (*pfn)()) clears the PCmdImgs and sets its creation function to the passed value. The next time the pointer is dereferenced, a new SCmdImgs-derived will be created.

As a result, there is a single SCmdImgs alive, that can be retrieved through SCmdDraw::GetCmdImgs(), but its type can be set at runtime. That is: if you deploy a number of "drawer"s, you can also design an interface to let the user to select his preferred.

Added Features

NOTIFY_xxx_HANDLER macros

They are the ATL-like messagemap macros used to dispatch WM_NOTIFY messages. They have been easily improved to accept an additional "type" parameter.

The new form is now GE_NOTIFY_xxx_TYPEDHANDLER( ... , func, type). (Note: depending on the particular macro, the "..." are code, id, range of IDs, or a combination of these). This allows to specify in the macro the type to which it will be cast, the LPNMHDR carried by the LPARAM message parameter.

This allows you to declare message-handlers to have - as a parameter - directly a reference to the required structure (for example, NMLISTVIEW&), not a LPNMHDR to be cast in the function body.

Typed notifications

To let Windows to reciprocally signal events, Windows provides a message dispatching architecture based on messages (MSG) and some API to send (SendMessage), post (PostMessage), retrieve (GetMessage, PeekMessage) and dispatch (DispatchMessage, WINDOWPROC). In this framework, WINDOWPOC is always an internal subclassing window procedure, and dispatching is done with message maps. Sending messages require instead more attention.

If sending already defined messages, we can simply call the SendMessage API passing the required parameters. If sending other kinds of messages, we need - at least - to define a way to identify them. This can be done by defining some manifest constant like #define WM_MYMESSAGE (WM_USER+xxx), but, imagine a source composed by various library modules and different components, may be from different developers. There is the need of a very strict numbering convention (to avoid reuse of same IDs in different sources) or something that can automate this.

NUtil::XId_base provides a static function (UINT NewVal()) that returns the value of an incremented static counter on every call.

NUtil::SId<T> provides a UINT _getval() function, that returns the value of a static variable initialized to XId_base::NewVal() the first time _getval is called. It also has an operator UINT() returning that value. This allows to associate as many UINTs to as many T types we may want to use with SId.

SNmHdr<N> is a struct having a NMHDR as a first member, initializing its "code" member with (UINT)SId<N>(). Windows used to define the WM_NOTIFY codes in the "commoncontrol" library in the form (OU - xxxU) (hence: from 0xFFFFFFFF down to ... about 3000 codes). Since I made XId_base to start from 0x2000 going up ... there are lots of IDs that can be used.

SNmHdr<N> has also a LRESULT Send(HWND hTo) function, that does a SendMessage(hTo, WM_NOTIFY, nmhdr.idFrom, (LPARAM)this);. We can so derive a struct (say SMyNotification) from SNnHdr<SMyNotification>, filling its members as required, and call Send.

To retrieve such a message, I provided some messagemap macros (GE_NOTIFY_REGISTREDHANDLER, GE_NOTIFY_CODE_REGISTREDHANDLER, GE_NOTIFY_RANGE_CODE_REGISTREDHANDLER) taking a "type" parameter, checking uMsg == WM_NOTIFY, and GE_::NUtil::SId<type><TYPE>() == ((LPNMHDR)lParam)->code), calling a function as lResult = func((int)wParam, *(type*)lParam, bHandled).

So we can place, for example, a GE_NOTIFY_CODE_REGISTREDHANDLER(OnMyHandler, SMyNotification) entry in the message map of a window to call the LRESULT OnMyHandler(int nID, SMyNotification& myntf, bool& bHandled) member function.

Command Routing

Imagine having a frame window with a child view inside. Menu and toolbars normally belong to the frame, and send WM_COMMAND and WM_NOTIFY to the frame.

But, you may be interested to handle those commands from a child view. You can do this by chaining the massage map of the frame to an alternate message map of the view (but, this means to transfer all messages). Or you can forward only WM_COMMAND or WM_NOTIFY.

That's what the GE_ROUTE_MSG_MAP_xxxx macros are for. You can route to a Class, to a Member, or through a Pointer (NULL pointer check is done).

There are two distinct series of macros for commands and notify. But remember, if you use autoupdate commands (GE_COMMAND_ID_HANDLER_U), that autoupdate is itself a WM_NOTIFY message. So, if you route commands ... route also WM_NOTIFY in the same way. Or use the macros that call both the series at the same time.

Note also that "ROUTE" macros call some::ProcessWindowMessage. This is not like "sending" a message: if a window has multiple wrappers, calling SendMessage makes all wrappers to be able to receive the message in their default message map, while "ROUTE" makes only the passed wrapper to handle the routed messages or commands.

If you want to re-send a command to a given window, instead of "route" it to a given wrapper, use GE_FORWARD_COMMANDS instead. It resends WM_COMMAND and WM_NOTIFY.

Forwarding messages

Another way to tell a window to handle a message originally sent to another one (without confusing it with the ones intended for that window) is "forwarding by encapsulation". The original message is re-sent inside another message to another window. This trick comes with ATL (ATL_FORWRARD_MESSAGE), but here, I generalized it.

GE_FORWARD_MESSAGE(hWndTo, code) sends a WM_GE_FORWARDMSG, whose parameters are a code ID (as WPARAM) and a NWin::XWndProcParams* (as LPARAM: it carries the original message parameters).

You can handle it in a destination HWND wrapper message map using GE_WM_FORWARDMSG(func), where "func" is a LRESULT func (NWin::XWndProcParams& msg, DWORD nCode, bool& bHandled);.

Or ... you can "decapsulate" the original message by recursively calling ProcessWindowMessage after a parameter extraction from XWndProcParams.

That's what can be done with:

GE_WM_FORWARDMSG_ALT(msgMapID): decapsulate the original message and make it available to the same message map, in another ALT_MSG_MAP section. This makes you able to handle the message with the GE_WM_xxx crackers.

GE_WM_FORWARDMSG_ALT_CODE(code, msgMapID): like before, but only those messages whose capsule have been tagged by "code" while resending.

Message Maps chaining

The GE_CHAIN_MSG_MAP series has been arranged to have a consistent number of macros: you can chain a default map or a particular "alternate map" (xxx_ALT(..., msgMapID): same concept than ATL).

And you can chain to:

  • A class: useful for derived classes to refer their bases.
  • A member: useful for classes that host inside themselves as members, other classes.
  • A pointer: useful in more complex structures, where various references exist. Null pointers are checked before calling the pointed ProcessWindowMessage.

Message handling considerations

Combining all this message routing techniques, it is now possible to do almost everything. And considering that every class can be a NWin::IMessageMap derived (no need to be itself a window wrapper), message maps can be a useful way to let classes to communicate without the need to entangle themselves (to be designed to know their reciprocal interface) in a static way.

Where more dynamicity is required, the right solution - can be instead - to use "events" (NWrp::Event<A> and NWrp::EventRcv<D>: see part one for a description).

Note the main difference between the two methods: message maps are macros providing fragments of code. Events are data structures. Message map chains are defined at compile time (when translating the macros). Event dispatching is a completely runtime mechanism.

It is - of course - technically possible to define communication between classes through message maps, and is also possible to convert Windows messages into events. I don't think - however - that the way I support events can be used as-is with messages: chaining message maps allow a number of messages to pass from one map to another. Events - by now - are one to one: a receiver must register individually all events it wants to receive.

Finding a wrapper of a given type (RTTI)

Consider a window that can have a number of attached wrappers. You may be interested in seeking one of a given type (or inheriting from a given type). Because HWND wrappers are chained, this can be easily done using a template function and a dynamic_cast, by walking the chain until the cast is non-NULL. More general, bool WrpChainBase::DynamicFind<A>(A*&, TT::IH) does exactly that. But it is implemented in WrpChainBase, so it works on every chained wrapper.

Of course, because we are basing on dynamic_cast, RTTI must be enabled.

Modal Dialog

Modal dialogs are an asymmetry in the Windows API: the DialoBox Windows function, requires a DLGPROC, but that "proc" is not a real true WNDPROC.

The real WNDPROC is private to the system. It calls your procedure and - if returning false - calls DefDlgProc. All this is inside a modal loop, internal to the DialogBox API.

To reconduct this in an already existent wrapper, I implemented a EWnd::CreateModalDialog, that - acting as CreateWnd - sets a hook and passes as a DLGPROC an internal hidden function. The hook attaches the creating new window (dialog, in this case) to the requesting wrapper and auto-detaches itself.

The hook procedure has been reviewed to let the hook active for the shortest possible period (avoiding to recourse in the hook function where multiple creation of windows happen in a nested way - think to a parent that creates its children during its own creation process). The provided hidden DLGPROC always returns false, apart for WM_COMMANDs with code between 1 and 7 inclusive (IDOK, IDCANCEL, ..., IDCLOSE: just to have a default processing that returns. Or you'd risk to stuck your program into an "about box"!).

The reviewed hook procedure also corrects a bug: in the previous version, if a still-creating window is wrapped trapping WM_CREATE to create more windows (think to a main window with children), a number of nested hooks are instantiated, but - when returning - only the last is unhooked. Although this has no consequence in functionality (hook procedures do nothing but attach the first wrapper), it may have consequences on performance in certain cases. Now, this cannot anymore happen: the hook "unhooking" is done inside the hook proc itself, before any message is dispatched to any wrapper. No recursions can arise.

Putting all at work

It's not easy to demonstrate all this in a simple app that does more or less nothing but lets you check almost anything.

The W3 project uses both Nlib and NGUI in doing so. I started creating a frame, wrapping it with EMenuUpdator and EMauImagerIDE, and adding a child window during WM_CREATE.

I also routed commands to the child, and processed ID_FILE_EXIT in the main window and ID_HELP_ABOUT in the child (OK: that's unusual, but to demonstrate command routing, it's fine!).

To respond to ID_HELP_ABOUT, I instantiate a modal DialogBox.

Using Commoncontrols

There is the need to link the ComCtrl32.lib inport library, and call InitCommonControlsEx. This is an annoying always needed stuff, so I placed all into a class (NUtil::SInitCommonControlsEx). Just instantiate a temporary object calling the constructor passing the required value (I assumed to default to ICC_WIN95_CLASSES), and that's it. The library is linked through a #pragma comment(lib ...) in the NGUI/CommCtrl.h header.

Note: I didn't make this initialization implicit (i.e., via a static instantiated object) because it is not necessarily true that every application needs common controls in the same way.

Just to do something with the "About" dialog, I wrapped it with CAboutBox, and I instantiated on creation a timer that does a countdown with a progress bar. When reaching zero, the dialog auto-dismisses. (By pressing OK, you anticipate the dismissing). This is just to demonstrate the messagemap correctly working with a DLGPROC.

Handling more complex layouts

The idea is to let a frame manipulate a client window and a set of docked "bars", with an algorithm similar to the IDE.

DockMan.h and DockMan.cpp contain the required stuff.

In particular, two interfaces define the interactions between dockable objects and frames.

Dock Manager

ILayoutManager defines the prototype for the RedoLayout function, while IAutoLayout prototypes the DoLayout and GetSideAlignment functions. Typically, an implementation of ILayoutManager should contain or refer a number of IAutolayout elements to arrange when moved or sized. RedoLayout is called passing the requesting HWND (in case it is to be skipped by the layout algorithm. Normally this parameter is NULL). It should call, in turn, the DoLayout of the contained IAutoLayout, passing a rectangle. The contained IAutoLayout should redesign itself basing on that rectangle end modifying it to the portion of the rectangle that remains uncovered.

In synthesis, ILayoutManager is what defines how a layout should be done. IAutolayout are the components that are laid-out.

The provided implementation - to avoid to entangle the classes - splits the implementation of ILauoutManager into two parts.

An internal class (XLayoutProvider) implements on the heap an ILayoutManager, and can be gotten calling the static ILayoutManager::Get(HWND) function: it retrieves (via DynamicFind) an ILayoutManager associated to the passed HWND or (if none is attached) creates an XLayoutProvider and attaches it, after making it an auto-deletable observer.

XLayoutProvider processes the WM_SIZE message by getting the client rectangle and passing it into a typed notify message whose data structure is ILayoutManager::Autonotify.

At this point, you can attach an arbitrary number of wrappers implementing a handle to this message that, getting the rectangle carried by the data structure and some owned data, decides what to do with any number of embedded or referred IAutoLayout elements.

In particular, EDockBarManager implements ILayoutManager::Autonotify by containing one EClientWnd and four (one per side) EDockBars. Each EDockBar can receive any number of HWND to embed, and - when doing so - attaches an EDockBar::XElement to the passed HWND. This other internal class, is deigned to work with EDockBar, and it is an observer auto-deleting EWnd. It lives on the heap and is destroyed when the window it is attached to is destroyed, and maintains the docking state (the placement), managing docking and undocking of the wrapped HWND. The docking capability, so, it is not to be designed within the passed window, but is "plugged in" when the window's attached.

Those HWNDs don't need to be any of particular: just regular popup windows created as owned by the parent of the EDockBars (usually an EDockBarManager). They become children of the bars while docked and return to be popup when floating.

Of course, those windows can be controls or toolbars or ... other more complex windows (part 4 of the article will treat this theme).

About commands, both EDockBar and EdockBar::XElem forward received commands to the parent (or owner), while EDockBarManager forwards them to the client window. This creates a sort of MFC-like command routing.

The sample application

In the sample app, I create a CMainWnd that - in turn - creates 8 differently positioned bars (see CMainWnd::OnCreate). Some are movable, some others are sizable.

Some bars have a regular "close button". If you "close" them, it will act as normally, destroying the bar (and since it is wrapped by an internal auto-deleting observing wrapper, the wrapper is also destroyed). I didn't provide any interface to manage bar creation or hiding, since it was not in the scope of these classes.

Note the NUtil::STrace::_Filter() = 2; in WinMain: it is to avoid the STrace class to display, in the debug output, lots of messages coming from the message dispatching and from the GDI Handle wrapping-unwrapping activities.

Further work

I'm actually working on a unified model for toolbar, statusbar, menubar etc., and for an IDE-like interface for inner windows. They'll be the subject for part 4.

History

  • Posted 27 May 2004.
  • Bug Fixing: C4346 (typename missing in Misc.h): posted on 18 June 2004.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here