What is it & Why?
My real intention, when I started this project, was to develop a tree
control to display network resources. Enumerating network resources could be a
lengthy operation, so I decided to actually do enumeration only when the user
tries to expand an item. I realized soon that this kind of behavior could be
useful to many other purposes, so I put all the code needed to have this
behavior in a base class for my tree control, that was ready to use with other
classes. I also added support for visual feedback, while an item is being
populated.
When the
user clicks to expand an item, a new item is added which contains a wait
message. If you implement some animation inside the tree control the message is
visible, otherwise it remains hidden. The blue rectangle is the area in which
you can draw: you can do everything you want, even completely overwrite the
wait message, as long as you stay within that area. Timed animations are also
supported.
In the meantime, you can populate the item being expanded with new child
items and the tree control will redraw itself to reflect the new content only
when you have finished: the user will see only your animation. You may also
choose not to display anything while populating, this is very easy.
I also added the ability to repaint the control while populating. The main
thread is busy until the population process completes, this is "by
design", but I noticed that it still receives WM_ERASEBKGND
messages, at least on Win2K. So now I take a snapshot of the control before the
process takes place and use that bitmap to redraw the control's background
until the expanded item gets populated completely. (Please, report if this works
on other platforms)
If you want to see some examples of what you can do with
this class take a look at these articles:
Using the Class
This class provides a common interface for two types of derived classes:
those providing animation effects and those providing the items for the tree
control. Obviously you can implement a single class that provides both content
and animation, but you may lose in code re-use.
If you keep the two implementations separated, you can:
- focus your attention on the content provider class, which will
handle the underlying tree control
- choose a ready-to-use animation provider class to add visual
feedback to your content provider class or create your own
- leave the control without any animation (users won't see the wait
message)
- enable the wait message when you use your content provider class
Providing Tree Content
Just derive your class from CTreeCtrl and then replace each
occurence of CTreeCtrl with CWaitingTreeCtrl in both
your '.h' and '.cpp' files.
class CMyTreeCtrl : public CWaitingTreeCtrl
{
...
};
Tip: if you want to use Class Wizard to generate message handlers
later, you may temporarily change the base class in the BEGIN_MESSAGE_MAP
macro to CTreeCtrl, but remember to restore it to the correct one
(CWaitingTreeCtrl) and to change every call to the base
implementation in the generated message handlers.
BEGIN_MESSAGE_MAP(CMyTreeCtrl, CWaitingTreeCtrl)
...
END_MESSAGE_MAP()
To fill in the tree control with items you are required to override PopulateItem
and to call PopulateRoot somewhere to insert items at the first
level. You may also override WantsRefresh to automatically refresh
some items whenever they are expanded by user.
CWaitingTreeCtrl::PopulateItem
virtual void PopulateItem(HTREEITEM hParent)
This function gets called in the main thread after an item is expanded and
the wait message is displayed. The animation takes place during the execution
of this function and it is stopped when the function returns.
Tipically, you use the hParent parameter to get information
about the parent item to populate and then you start adding new items. No
redraw will happen until this function has finished, this is by design.
Also, there's currently no mean to abort this function, because it blocks
the main thread, but you may create the underlying tree control in another user
interface thread and provide your own means.
When you implement this function you have to remember the following rules:
- you are supposed to add only one level of child items and you should
only work with the
hParent item and its children
- you can always suppose that the
hParent item has no
children when the function gets called
- you need to handle the special case in which
hParent is TVI_ROOT
- you should set the
cChildren field of the TVITEM
structure to <SPAN CLASS="cpp-literal">1 if you want
that child item to be expandable
- you should notify your progress to the base class (see below)
Returning TRUE means that the base class will check if you
added any items and if it doesn't find any you will not be able to expand the hParent
item again (the plus button will be removed).
Returning FALSE means that the hParent item will
be always expandable, even if it has no children now. Note that it doesn't mean
that the hParent item will be refreshed every time it's expanded,
but that it will be expandable until it gets some children. If you want to
refresh the hParent item each time it's expanded you have to
override WantsRefresh.
Note: if you associate some data with each item you will probably
handle the TVN_DELETEITEM notification. If you do so, you should
ignore items that have the lParam field equals to zero. They could
be the items used to display the wait message and they have no associated data.
However this should not be a problem, since a zero value is tipically a NULL
pointer.
CWaitingTreeCtrl::WantsRefresh
virtual BOOL WantsRefresh(HTREEITEM hParent)
This function gets called just before PopulateItem, only if the
hParent item already has children, to ask a derived class if it
wants the item's children to be refreshed.
You have to override this function if you want an item to refresh its
children whenever the user expand it. Remember that to refresh items that have
no children, you have to tell the base class not to check for inserted items by
returning FALSE in your PopulateItem override.
Return TRUE if you want to automatically refresh the item (in
the sense described above), or FALSE if you don't want automatic
refresh. The base class implementation simply returns FALSE.
CWaitingTreeCtrl::PopulateRoot
void PopulateRoot()
You have to call this function somewhere in your derived class, if you want
to see some items. A good place could be in your PreSubclassWindow
or OnCreate override, but you may decide to have a function which
initializes some data associated with the root item and then simulates a root
expansion, populating the first level of items (PopulateItem is
called with TVI_ROOT as the parent item).
You may also populate the first or some deeper levels of the tree without
requiring the user to expand any items. A parent item that already has children
won't be passed to PopulateItem, unless WantsRefresh
says it must be refreshed. So static items are perfectly legal and can be used
in conjunction with dynamic items.
CWaitingTreeCtrl::SetPopulationCount
void SetPopulationCount(int iMaxSubItems, int iFirstSubItem = 0)
You should call this function in your PopulateItem override,
before adding any item, to set the total count of items you plan to insert (iMaxSubItems)
and the optional initial value (iFirstSubItem).
You should set the total count to zero if you don't know how many items you're
going to insert.
CWaitingTreeCtrl::IncreasePopulation
void IncreasePopulation(int iSubItemsToAdd = 1)
You should call this function in your PopulateItem override,
when you insert a new item or a group of items. The current count is increased
of the iSubItemsToAdd parameter, which can be a negative value.
CWaitingTreeCtrl::IncreasePopulation
void UpdatePopulation(int iSubItems)
You should call this function in your PopulateItem override,
when you insert a new item or a group of items. The current count is updated to
the value of iSubItems.
Providing Animation
Create a generic class and derive it from CWaitingTreeCtrl.
Later you may replace the base class with a template, so that you can use the
class as a plug-in to add custom animations and visual effects to a generic CWaitingTreeCtrl-derived
class. (I'm not familiar with templates, so there could be a better way to do
it).
template <class BASE_TYPE>
class CMyAnimationFX : public BASE_TYPE
{
...
};
There are two types of animations: those refreshing at specified time
intervals and those getting updated only when new items are inserted by a content
provider class.
To display an animation frame, you have to override DoAnimation.
If you want to initialize some variables related to the message item, such as
the position of the animation frames, you need to override PreAnimation
and PostAnimation (only if you need to free some memory at the
end).
In all these functions you should call the base class implementation,
especially when not directly deriving from CWaitingTreeCtrl. This
way, and using templates, you can add more than a single animation to your content
provider class.
CWaitingTreeCtrl::PreAnimation
virtual void PreAnimation(HTREEITEM hItemMsg)
This function gets called in the main thread just before the animation
starts. Its only argument is the handle of the tree item displaying the wait
message. You may use it to calculate the position in which to show the
animation, but you shouldn't store it for later use. You can't assume the item
still exists during the animation.
The wait message item is not visible by default, if you want to use its
attributes to display something near to it, such as an animated item's image,
or if you change its visual aspect, you have to call ShowWaitMessage
in the constructor (or somewhere before entering this function).
You usually implement this function to initialize additional variables you
may need during the animation. Remember that the hItemMsg item is
a temporary item: you can use it only from inside this function, and only if
the wait message is visible.
If you use the hItemMsg to draw something, you need to make it
visible. If the message item is visible, you may also change the visual aspect
of the tree control, such as scrolling the client area to properly displaying
the animation. If the wait message is hidden the tree control can't redraw
itself. You may have this situation if you don't draw the animation inside the
tree control, but instead you use another window or control.
CWaitingTreeCtrl::DoAnimation
virtual void DoAnimation(BOOL bTimerEvent, int iMaxSteps, int iStep)
This function gets called in a higher priority worker thread each time you
need to update your animation. If the bTimerEvent argument is TRUE
this function has been called when the timer interval elapsed, otherwise the
item count has been updated. If you want to handle timer events you have to
specify the timer period calling SetAnimationDelay before the
animation starts, either in the constructor or in PreAnimation.
The other two arguments reflect the current progress of the enumeration
process: iMaxSteps is the total number of steps of the process
(zero means unknown), while iStep is the current step. You may
choose to ignore these during a timer event. You tipically use this information
to display a progress bar.
You need to implement this function to draw a new frame of your animation.
If you draw within the tree control, you should only draw inside the rectangle
obtained by calling GetItemRect(hItemMsg, lpRect, FALSE). Remember
that you can't call this function here: you may use CTreeCtrl::GetItemRect
or CWaitingTreeCtrl::GetItemImageRect only inside PreAnimation.
CWaitingTreeCtrl::PostAnimation
virtual void PostAnimation()
This function gets called in the main thread just after the animation ends.
You have to implement this function only if you need to clean up some
additional variables.
CWaitingTreeCtrl::SetAnimationDelay
void SetAnimationDelay(UINT nMilliseconds)
You may call this function in the constructor, or in your PreAnimation
override, to set the delay between timer events. A delay of zero means you don't
need timer events and it is the initial value. If you want timer events you
have to explicitly call this function.
Note that there is only one timer, so the last call to this function before
the animation starts takes precedence (if you want to use multiple animation
classes).
CWaitingTreeCtrl::GetItemImageRect
BOOL GetItemImageRect(HTREEITEM hItem, LPRECT pRect)
You may call this function if you need to draw over an item's image, usually
beside the wait message.
The return value is TRUE if the function is successful, FALSE
otherwise.
Public Functions
There are also a few public functions that your content provider
class will inherit.
CWaitingTreeCtrl::SetWaitMessage
void SetWaitMessage(LPCTSTR pszText, HICON hIcon = NULL)
You may call this function to change the wait message's text and image. If
the hIcon argument is NULL the item gets a blank
icon.
CWaitingTreeCtrl::ShowWaitMessage
void ShowWaitMessage()
You need to call this function in your animation provider class,
either in the constructor or in any place before PreAnimation gets
called, if you want to draw within the tree control. You are supposed to draw
only in the rectangular area occupied by the wait message, so it must be
visible. If you draw your animation in another window or if you use another
control to provide visual feedback, you are not required to make the wait
message visible.
You may also call this function if you don't have any animations, but you
want the static wait message. Never call this function in your content
provider class, the choice to display the message is left to the final user
of your tree control.
Remember that the animation provider class should always assume that
the wait message is not visible, and so it must show it if it needs to draw
inside the control.
CWaitingTreeCtrl::RefreshSubItems
void RefreshSubItems(HTREEITEM hParent)
You may call this function to manually refresh the hParent
item's children. You can only refresh items that can be expanded, whether or
not they actually have children. In fact you assign a button only to items that
can be expanded by the user, and only those items can be refreshed.
Updates
17 Oct 2000
- Initial public release.
27 Sep 2001
- Fixed a bug with refreshing empty items and possibly some other things
- Added background repainting while busy (at least on Win2k)
- License changed to Artistic License
Conclusion
This class probably needs some adjustments, but it's enough for me now. I
think you may better understand this class if you see some implementations, so
take a look at the other articles (see top). I will
appreciate any comment, suggestion or contribution. Any help to improve this
(and the others) article is welcome, I know it's not easy for you to read and
it has been difficult for me to write.