在开发一些窗口层次比复杂的cocos2d项目时,会发现一些由于没有窗口层次而引起的bug。这些bug让一些从windows平台过来的人觉得很无奈。比如,我们有一个列表控件,我们在其中放了一些菜单,当我们滑动列表控件使菜单选项(称为A)滑出列表控件的可视范围时,按理我们是无法再点击到A的,因为它滚动出了父控件可视范围,不被用户所看到。但是cocos2d的默认情况是能点击到的,这是因为cocos2d的消息管理是优先级消息机制,只要控件登记接收消息,那么cocos2d会一如既往的发给他。所以我们刚才讲的情形在cocos2d看来,它无法根据A被遮挡而不给A发消息。究其根本,是没有一个层级窗口消息机制(当然你能通过其他的方法帮助cocos2d,但我个人觉得有点不够彻底)。
我想建立一个相对完整cocos2d的的层级窗口消息机制,且不改变cocos2d任何源码(因为改变源码的话,不知道以后升级起来是不是很麻烦)。基本思路有如下几条:

  1. 在任何一个场景中,我们会有一个最底层的Layer(我称为祖层),这个Layer将接受cocos2d的触摸消息。并且这个Layer能将触摸消息传递给其所有的子控件。
  2. 一个场景中除了祖层之外,所有其他的控件都将不接受任何触摸消息,其触摸消息的来源于父控件。当然任何非叶子控件都会将其触摸消息传递给其子控件。
  3. 控件将消息传递给子控件时,如果子控件消息处理函数返回真,则记录该子控件,以方便将后续消息传递给这个子控件。
  4. 尽可能兼容已知和未知的cocos2d控件库,这样在这个机制类我们会有更多好用的子控件。
  5. 提供模式和非模式两种窗口模式。就像我们在用windows时,点新建文件,会弹出一个模式窗口,不关闭它,我们无法和其他窗口交互。

下面看一下类的组织架构图:
cocos2d-x消息机制
我们分别说一下各个类的大体作用:

  1. wmTouchDelegate并非继承于ccTouchDelegate或其他类(但消息处理函数名同ccTouchDelegate一样)。它是一个消息传播大使,所有继承于该类的类都能自动地将消息传播到所有的子控件。不继承于ccTouchDelegate主要出于设计原则中避免多重继承中的基类重复。
  2. wmLayer继承于wmTouchDelegate和CCLayer(图中未指出),负责将消息处理函数转接到wmTouchDelegate的消息处理函数,这是因为wmTouchDelegate的消息处理函数同ccTouchDelegate是一样的,而CCLayer已经继承了ccTouchDelegate,这样如果不显示的转接处理函数,C++编译器会提示有两个版本选择的错误。
  3. wmLayerAncestor继承于wmLayer,这样它已经具备了消息转发的功能。同时它接受触摸消息。并注册了优先级为0且吞并消息的touch target。这样它将是上面提到的祖层,一个Scene中只有一个祖层。
  4. wmLayerDescendant继承于wmLayer,这样它已经具备了消息转发功能,同时它屏蔽了消息触摸消息。一个scene中多数层都应该是继承与wmLayerDescendant的。
  5. wmLayerModal继承于wmLayerAncestor,所以它接受触摸消息,为了达到模式窗口的功能(程序中当有模式窗口时,只有最顶层的模式窗口能接受到消息,其他层都屏蔽),我们将wmLayerModal注册为优先级-128且吞并消息的窗口。

通过这个类层次,我实现了在cocos2d-x 2.0.4中的CCTableView中放多层元素,且元素滚动出View可视范围无法点中的功能,且点击中菜单时,TableView依旧能滚动的功能(因为原来的CCTableView中放CCMenu的话,一旦点击中菜单,就无法拖动TableView了,因为CCMenu把消息吞了)。且可与CCControl等cocos控件一同使用。
cocos2d-x消息机制
下面来看源码:
wmTouchDelegate:
wmTouchDelegate.h
//
// Created by jason on 12-12-25.
//
//
#ifndef __TableTest__wmTouchDelegate__
#define __TableTest__wmTouchDelegate__
#include
#include "cocos2d.h"
USING_NS_CC;
#define WM_TOUCH_DELEGATE_OWNER_CONSTRUCTOR_IN_HEAD_FILE( ownerClassName ) \
ownerClassName() : wmTouchDelegate( this ){}
#define WM_TOUCH_DELEGATE_IMPLEMENT_IN_HEAD_FILE() \
virtual bool ccTouchBegan(CCTouch *pTouch, CCEvent *pEvent) \
{ \
return wmTouchDelegate::ccTouchBegan( pTouch, pEvent ); \
} \
virtual void ccTouchMoved(CCTouch *pTouch, CCEvent *pEvent) \
{ \
wmTouchDelegate::ccTouchMoved( pTouch, pEvent ); \
} \
virtual void ccTouchEnded(CCTouch *pTouch, CCEvent *pEvent) \
{ \
wmTouchDelegate::ccTouchEnded( pTouch, pEvent ); \
} \
virtual void ccTouchCancelled(CCTouch *pTouch, CCEvent *pEvent) \
{ \
wmTouchDelegate::ccTouchCancelled( pTouch, pEvent ); \
}
//window message mechanism
//We only change the TargetedTouch message mechansim.
//Node who want standard touch message should register all by itself.
class wmTouchDelegate
{
public:
enum
{
MAX_NON_TRIVAL_TOUCH_HANDLER_CLASS = 256,
};
public:
wmTouchDelegate( CCNode* pOwner ) :
m_pOwner( pOwner ),
m_bDraging( false )
{
m_pItemsClaimTouch = CCArray::createWithCapacity( CHILD_MAX );
assert( m_pItemsClaimTouch );
m_pItemsClaimTouch->retain();
}
virtual ~wmTouchDelegate()
{
CC_SAFE_RELEASE_NULL( m_pItemsClaimTouch );
}
protected:
// default implements are used to call script callback if exist
virtual bool ccTouchBegan(CCTouch *pTouch, CCEvent *pEvent);
virtual void ccTouchMoved(CCTouch *pTouch, CCEvent *pEvent);
virtual void ccTouchEnded(CCTouch *pTouch, CCEvent *pEvent);
virtual void ccTouchCancelled(CCTouch *pTouch, CCEvent *pEvent);
private:
//return value:
//true: pParent is touched by user
//false: pParent isn't touched by user.
bool passMessage( CCNode* pParent, CCTouch *pTouch, CCEvent *pEvent );
//if has significant touch handler.
bool hasNonTrivalTouchHandler( cocos2d::CCNode *pItem );
private:
CCNode* m_pOwner;
bool m_bDraging;
//items claim touch message
CCArray* m_pItemsClaimTouch;
//store all class whose touch handler is non-trival
static const void* m_pNonTrivalTouchHandlerClasses[];
};#endif /* defined(__TableTest__wmTouchDelegate__) */

代码分割
//
// wmTouchDelegate.cpp
// TableTest
//
// Created by jason on 12-12-25.
//
//
#include "wmTouchDelegate.h"
#include "cocos-ext.h"
#include "wmControl.h"//include wmTableView,wmMenu,wmControl
USING_NS_CC_EXT;
const void* wmTouchDelegate::m_pNonTrivalTouchHandlerClasses[ ] =
{
&typeid( wmTableView ),
&typeid( wmMenu ),
&typeid( wmControlButton ),
&typeid( CCControlButton ),//if you want to let the cocos2d original control work in our scheme. But I suggest you subclass the original control so that it won't register with touch dispatcher.
};
#pragma mark- input touche
bool wmTouchDelegate::ccTouchBegan(CCTouch *pTouch, CCEvent *pEvent)
{
//pass message to all children
return passMessage( m_pOwner, pTouch, pEvent );
}
void wmTouchDelegate::ccTouchMoved(CCTouch *pTouch, CCEvent *pEvent)
{
//special process for menu, we won't pass ccTouchMoved message to menu. Because we think menu doesn't need ccTouchMoved message in ios device where user always want to dray layer instead menu. The fllowing block for menu will only go once.
if( false == m_bDraging )
{
for( int i = 0; i < m_pItemsClaimTouch->count(); )
{
CCLayer* pItem = ( CCLayer* )m_pItemsClaimTouch->objectAtIndex( i );
//menu items doesn't process ccTouchMove(), cancel it.
assert( NULL != pItem );
//if it's menu
if( dynamic_cast< CCMenu* >( pItem ) )
{
pItem->ccTouchCancelled( pTouch, pEvent );
m_pItemsClaimTouch->removeObjectAtIndex( i );
}
else
{
++i;
}
}
}
//pass ccTouchMoved message to un-CCMenu item
int iNumItemsNotMenu = m_pItemsClaimTouch->count();
for( int i = 0; i < iNumItemsNotMenu; ++i ) { wmTouchDelegate* pItemWindowMessage = NULL; CCLayer* pItem = NULL; pItem = ( CCLayer* )m_pItemsClaimTouch->objectAtIndex( i );
assert( NULL != pItem );
//window message items
if( ( pItemWindowMessage = dynamic_cast< wmTouchDelegate* >( pItem ) ) )
{
pItemWindowMessage->ccTouchMoved( pTouch, pEvent );
}
else//coscos2d-x items
{
pItem->ccTouchMoved( pTouch, pEvent );
}
}
m_bDraging = true;
}
void wmTouchDelegate::ccTouchEnded(CCTouch *pTouch, CCEvent *pEvent)
{
int iNumItems = m_pItemsClaimTouch->count();
for( int i = 0; i < iNumItems; ++i ) { wmTouchDelegate* pItemWindowMessage = NULL; CCLayer* pItem = NULL; pItem = ( CCLayer* )m_pItemsClaimTouch->objectAtIndex( i );
assert( NULL != pItem );
//window message items
if( ( pItemWindowMessage = dynamic_cast< wmTouchDelegate* >( pItem ) ) )
{
pItemWindowMessage->ccTouchEnded( pTouch, pEvent );
}
else//coscos2d-x items
{
pItem->ccTouchEnded( pTouch, pEvent );
}
}
m_pItemsClaimTouch->removeAllObjects();
m_bDraging = false;
}
void wmTouchDelegate::ccTouchCancelled(CCTouch *pTouch, CCEvent *pEvent)
{
int iNumItems = m_pItemsClaimTouch->count();
for( int i = 0; i < iNumItems; ++i ) { wmTouchDelegate* pItemWindowMessage = NULL; CCLayer* pItem = NULL; pItem = ( CCLayer* )m_pItemsClaimTouch->objectAtIndex( i );
assert( NULL != pItem );
//window message items
if( ( pItemWindowMessage = dynamic_cast< wmTouchDelegate* >( pItem ) ) )
{
pItemWindowMessage->ccTouchCancelled( pTouch, pEvent );
}
else//coscos2d-x items
{
pItem->ccTouchCancelled( pTouch, pEvent );
}
}
m_pItemsClaimTouch->removeAllObjects();
m_bDraging = false;
}
bool wmTouchDelegate::passMessage( CCNode* pParent, CCTouch *pTouch, CCEvent *pEvent )
{
if( !pParent || !pParent->isVisible() )
{
return false;
}
CCPoint pt;
CCRect rcBoundingBox;
//hande message to items
int iNumChildren = 0;
CCArray* pChildren = NULL;
//if the item'size > 1, check whether use touches it. Such as TableView.
//some items doesn't get size. they are medium for maintaining some children. Such as CCTableViewCell.
if( pParent->getContentSize().width * pParent->getContentSize().height > 1.0f )
{
pt = pParent->convertTouchToNodeSpace( pTouch );
rcBoundingBox.setRect( 0, 0, pParent->getContentSize().width, pParent->getContentSize().height );
//whether hit the node
if( !rcBoundingBox.containsPoint( pt ) )
{
return false;
}
}
pChildren = pParent->getChildren();
//no children, but use touch this item, so return true.
if( !pChildren )
{
return true;
}
iNumChildren = pParent->getChildren()->count();
//pass to all children
for( int iChildIndex = 0; iChildIndex < iNumChildren; ++iChildIndex ) { //if the item claims the touch message bool bClaim = false; wmTouchDelegate* pItemWindowMessage = NULL; CCNode* pItem = NULL; pItem = ( CCNode* )( pChildren->objectAtIndex( iChildIndex ) );
assert( pItem );
//items derives from wmTouchDelegate
if( ( pItemWindowMessage = dynamic_cast< wmTouchDelegate* >( pItem ) ) )
{
bClaim = pItemWindowMessage->ccTouchBegan( pTouch, pEvent );
}
else//items doesn't derive from wmTouchDelegate
{
//classes have non-trival ccTouchX() hander
if( hasNonTrivalTouchHandler( pItem ) )
{
bClaim = ( ( CCLayer* ) pItem )->ccTouchBegan( pTouch, pEvent );
}
//items who doesn't derive from wmTouchDelegate can't pass touch message to its children,
//so we have to help them to pass touch message.
passMessage( pItem, pTouch, pEvent );
}
//if this item is interested in this message, add it to array for other messages
if( bClaim )
{
m_pItemsClaimTouch->addObject( pItem );
}
}
return true;
}
bool wmTouchDelegate::hasNonTrivalTouchHandler( cocos2d::CCNode *pItem )
{
//classes have non-trival ccTouchX() hander
const void* pItemAddress = &typeid( *pItem );
for ( int i = 0; i < sizeof( m_pNonTrivalTouchHandlerClasses ) / sizeof( void* ); ++i) { if( m_pNonTrivalTouchHandlerClasses[ i ] == pItemAddress ) { return true; } } return false; }

其中比较重要的地方解释一下:

  1. 首先是m_bDragging是为了判断拖动用的,主要用于菜单处理,当点击了一下之后,菜单对此消息感兴趣,我们记录菜单,并传递后续消息,然后点击完之后传来的是TouchMoved消息,那么就停止菜单消息处理。
  2. passMessage同ccTouchBegan一起来完成消息递归传递。passMessage还将帮助没有继承wmTouchDelegate的类(这些类的ccTouchBegan不具备消息传递功能)传递消息给其子控件。
  3. hasNonTrivalTouchHandler()函数是用于判断哪些类具有重要消息处理函数的,这些类都是没有继承wmTouchDelegate,我们在帮助它把消息传递给子控件的同时,也让它调用自己的消息处理函数来完成自己的操作,这里目前只加了wmTableView,wmMenu这两个类,他们都是简单地继承ccTableView,ccMenu,并屏蔽触摸消息(因为任何加入窗口层级消息体制的控件都应该只能从父控件那里获得消息)。当然通过typeid来设计是并不好的,扩展性差,当添加一个新控件进去我们都要往这个函数中添加一个判断。但是由于我们基于不修改cocos2d源码的考虑,目前只能这么做。如果想做彻底,那么我们将会从ccNode那里开始设计。

wmLayer, wmLayerAncestor, wmLayerDescendant, wmLayerModal都在一个头文件中完成,没有cpp。因为主要功能都有wmTouchDelegate完成了,这些类只是做了简单功能和约束的的添加。
wmLayer.h
//
// Created by jason on 12-12-21.
//
//
#ifndef __TableTest__WMLayer__
#define __TableTest__WMLayer__
#include
#include "cocos2d.h"
#include "wmTouchDelegate.h"
USING_NS_CC;
#define WM_INIT_DEFAULT( parentClassName ) \
virtual bool init() \
{ \
if( !parentClassName::init() ) \
{ \
return false; \
} \
\
return true; \
}
#define WM_TOUCH_REGISTER_DEFAULT( iPriority ) \
virtual void registerWithTouchDispatcher( void ) \
{ \
CCDirector::sharedDirector()->getTouchDispatcher()->addTargetedDelegate( this, iPriority, true );\
}
//Users shouldn't derive from wmLayerAncestor, wmLayerDescendant, wmLayerModal instead wmLayer.
//wmLayer can't be touched.
//wmLayer pass message to it's all descendant.
class wmLayer : public CCLayer, public wmTouchDelegate
{
protected:
WM_INIT_DEFAULT( CCLayer );
WM_TOUCH_DELEGATE_OWNER_CONSTRUCTOR_IN_HEAD_FILE( wmLayer );
WM_TOUCH_DELEGATE_IMPLEMENT_IN_HEAD_FILE();
//static
public:
CREATE_FUNC( wmLayer );
};
//wmLayerAncestor can be touched.
//all secene should have only one wmLayerAncestor for bottom layer.
//all the other layer should be wmLayerDescendant.
class wmLayerAncestor : public wmLayer
{
protected:
bool virtual init()
{
if( !wmLayer::init() )
{
return false;
}
setTouchEnabled( true );
return true;
}
WM_TOUCH_REGISTER_DEFAULT( 0 );
//static
public:
CREATE_FUNC( wmLayerAncestor );
//data
};
class wmLayerDescendant : public wmLayer
{
protected:
WM_INIT_DEFAULT( wmLayer );
virtual void registerWithTouchDispatcher(){};
//static
public:
CREATE_FUNC( wmLayerDescendant );
//data
};
//wmLayerModal stopes touch messages from being passed to other layers which are not it's children.
//And will pass touch messages to it's children normally.
class wmLayerModal : public wmLayerAncestor
{
protected:
WM_INIT_DEFAULT( wmLayerAncestor );
WM_TOUCH_REGISTER_DEFAULT( kCCMenuHandlerPriority );
//static
public:
CREATE_FUNC( wmLayerModal );
//data
};
#endif /* defined(__TableTest__WMLayer__) */

里面通过一些宏,让我编写的更快一些。但希望没能阻碍你阅读。这里面没有太多需要说的。
整个体制是如何做到消息递归传递的呢?
答:任何一个wmLayer对象或者派生自wmLayer的对象,将记录每个直接子控件(对象的第一层子控件),并试图沿每个直接子控件路径逐层向下记录所有非直接子控件(不是这个对象的第一层子控件),直到路径上碰到继承自wmTouchDelegate子控件,我们称它为X(也记录这个)。这个子控件X将负责记录它的所有子控件(按照上面的方法,任何一个wmLaye…)。
也正是通过这个方法,我们能让一些没有继承wmTouchDelegate的控件也能加入这个体系,其消息传递功能由离它最近的上层继承自wmTouchDelegate控件来完成。
扩展提示:
如果你想让其他控件加入这个体制,简单的方法继承这些控件并屏蔽他们接受消息,我们的体制会确保消息能传递给他们身上的所有子控件。如wmMenu,wmTableView,wmControl,代码如下:
class wmTableView : public CCTableView
{
public:
static wmTableView* create(cocos2d::extension::CCTableViewDataSource *dataSource, cocos2d::CCSize size, CCNode *container = NULL )
{
wmTableView *table = new wmTableView();
table->initWithViewSize(size, container);
table->autorelease();
table->setDataSource(dataSource);
table->_updateContentSize();
return table;
}
void registerWithTouchDispatcher(void){}
};
//this menu won't get any touch message becase it doesn't register target touches.
class wmMenu : public CCMenu
{
public:
virtual void registerWithTouchDispatcher()
{
}
static wmMenu * create(CCMenuItem* item, ...)
{
va_list args;
va_start(args,item);
wmMenu *pRet = new wmMenu();
if (pRet && pRet->initWithItems(item, args))
{
pRet->autorelease();
va_end(args);
return pRet;
}
va_end(args);
CC_SAFE_DELETE(pRet);
return NULL;
}
};
class wmControlButton : public CCControlButton
{
public:
WM_TOUCH_REGISTER_EMPTY();
static wmControlButton* create(CCNode* label, CCScale9Sprite* backgroundSprite)
{
wmControlButton *pRet = new wmControlButton();
pRet->initWithLabelAndBackgroundSprite(label, backgroundSprite);
pRet->autorelease();
return pRet;
}
};

但如果想更好的优化消息记录,应该避免一个wmLayer上面放N多层 非本体制控件(即没有继承wmTouchDelegate的控件)。可以在其中穿插一些本体制的控件,因为这些控件具有消息传递功能。如何创建本体制控件呢?
1.第一种情况,类对象已经继承了CCLayer,且没有自己重要的消息处理函数。如CCLayerColor,我们想把它变成本体系控件,我们应该继承CCLayerColor,并继承wmTouchDelegate。然后转接消息处理函数。如下代码:
class wmLayerColor : public CCLayerColor, public wmTouchDelegate
{
protected:
WM_TOUCH_DELEGATE_OWNER_CONSTRUCTOR_IN_HEAD_FILE( wmLayerColor );
WM_TOUCH_DELEGATE_IMPLEMENT_IN_HEAD_FILE();
WM_INIT_DEFAULT( CCLayerColor );
//static
public:
CREATE_FUNC( wmLayerColor );
};

这是因为这类对象已经继承了CCLayer,我们不能让它再继承wmLayerDescendant,因为wmLayerDescendant也继承自ccLayer,这样多重继承中会有多份CCLayer,比较危险。所以我们简单的继承自wmTouchDelegate就可以了。
2.第二种情况,我们自己创造新型控件,如我们要在商店放一些物品,每个物品都是有一套共同的UI(物品图片,物品名称,价钱,购买数量,购买按钮),我们可以写 一个控件,然后重用这个控件将会让商店编程更简单。此时我们就让这个新控件继承自wmLayerDescendant即可。其他就不用管了。
class wmUISellItem : public wmLayerDescendant
{
//macro
public:
enum
{
ITEM_NAME_LENGTH = 20,
WIDTH = 219,
HEIGHT = 141,
};
//method
protected:
bool init();
//static
public:
static wmUISellItem* Create( const char* pszItemName, int iItemPriceNormal, int iItemPriceVIP, int iItemAmount );
};

对多触摸点消息感兴趣的控件,需自行注册standardTouchDelegate。我们没分将多触摸点消息转化成单触摸点消息传播,因为多触摸点消息更适合一次性接受处理,而不是分批传递。
最后,上述代码在写文章时做过手动修改,所以可能存在书写错误。如若使用需自行改正。这里面一定存在一些不足,还请指出改正,感激不尽。
感谢开发者xujiezhige的分享!