鍵盤輸入

鍵盤輸入在Cocos2Dx分爲兩個部分。第一部分是一些功能鍵的處理:後退鍵和菜單鍵。第二部分是處理字符輸入。功能鍵相關的代碼位於cocos2dx\keypad_dispatcher。字符輸入的代碼位於\cocos2dx\text_input_node。

我們先看功能鍵的處理部分。功能鍵的處理比較簡單,只是支持後退鍵和菜單鍵。而且功能鍵的支持只是對WP和Android有效。功能鍵部分的類關係如下圖所示。結構類似於觸控處理。只是少了一個Delegate接口。

CCKeypadDelegate定義了兩個功能鍵的處理接口。如果我們對處理功能鍵感興趣,我們只需要繼承CCKeypadDelegate,然後實現這兩個接口即可。CCLayer已經繼承了CCKeypadDelegate。我們在定義自己的層的時候,可以重載它即可。

我們還是從WIN32的窗口過程開始,來看功能鍵處理接口怎麼被調用到。CCEGLView::WindowProc:

1
2
3
4
5
6
7
8
9
10
11
12
    case WM_KEYDOWN:
        if (wParam == VK_F1 || wParam == VK_F2)
        {
            CCDirector* pDirector = CCDirector::sharedDirector();
            if (GetKeyState(VK_LSHIFT) < 0 || GetKeyState(VK_RSHIFT) < 0 || GetKeyState(VK_SHIFT) < 0)
                pDirector->getKeypadDispatcher()->dispatchKeypadMSG(wParam == VK_F1 ? kTypeBackClicked : kTypeMenuClicked);
        }
        else if (wParam == VK_ESCAPE)
        {
            CCDirector::sharedDirector()->getKeypadDispatcher()->dispatchKeypadMSG(kTypeBackClicked);
        }
        break;

CCDirector內部有一個CCKeypadDispatcher類型的成員。它就是Cocos2Dx裏面負責功能鍵處理的唯一對象。CCDirector有接口setKeypadDispatcher來替換它默認的功能鍵處理對象。但現在還沒有被使用到。系統將收到的功能鍵消息(退出鍵、菜單鍵)交給CCKeypadDispatcher的dispatchKeypadMSG處理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
bool CCKeypadDispatcher::dispatchKeypadMSG(ccKeypadMSGType nMsgType)
{
    CCKeypadHandler* pHandler = NULL;
    CCKeypadDelegate* pDelegate = NULL;
    m_bLocked = true;
    if (m_pDelegates->count() > 0)
    {
        CCObject* pObj = NULL;
        CCARRAY_FOREACH(m_pDelegates, pObj)
        {
            CC_BREAK_IF(!pObj);
            pHandler = (CCKeypadHandler*)pObj;
            pDelegate = pHandler->getDelegate();
            switch (nMsgType)
            {
            case kTypeBackClicked:
                pDelegate->keyBackClicked();
                break;
            case kTypeMenuClicked:
                pDelegate->keyMenuClicked();
                break;
            default:
                break;
            }
        }
    }
    m_bLocked = false;
    return true;
}

CCKeypadDispatcher::dispatchKeypadMSG內部遍歷所有的Handler,然後取出Handler包裹的Delegate,再根據功能鍵的類型,分別調用Delegate的keyBackClicked或keyMenuClicked。m_bLocked標記是爲了分發功能鍵消息的過程中,又有Handler被添加進來,或者刪除掉。CCKeypadDispatcher::removeDelegate和CCKeypadDispatcher::addDelegate在刪除和添加時,會檢查m_bLocked標記。如果現在正在分發消息,添加和刪除的Delegate都先暫存起來,分別放到m_pHandlersToRemove和m_pHandlersToAdd當中。當CCKeypadDispatcher::dispatchKeypadMSG完成消息分發以後,才真正從m_pDelegates中進行刪除或添加。相應的代碼由於篇幅的關係,沒有貼在這。

m_pDelegates中的Delegate是CCKeypadDispatcher::forceAddDelegate添加進來的。但它並不直接暴露給外部。暴露給外部的添加功能鍵處理Handler的接口是CCKeypadDispatcher::addDelegate。CCLayer在自己的成員函數CCLayer::setKeypadEnabled中,將自己註冊到CCKeypadDispatcher:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void CCLayer::setKeypadEnabled(bool enabled)
{
    if (enabled != m_bKeypadEnabled)
    {
        m_bKeypadEnabled = enabled;
        if (m_bRunning)
        {
            CCDirector* pDirector = CCDirector::sharedDirector();
            if (enabled)
            {
                pDirector->getKeypadDispatcher()->addDelegate(this);
            }
            else
            {
                pDirector->getKeypadDispatcher()->removeDelegate(this);
            }
        }
    }
}

跟觸控處理一樣,CCLayer默認也不是不開啓功能鍵功能的。

現在我們總結一下怎麼在遊戲中使用功能鍵。

方式一:使用自己的Layer。然後重寫keyBackClicked和keyMenuClicked函數,並且setKeypadEnabled(true)。

方式二:自己定義了一個處理類,讓它繼承自CCKeypadDelegate,並實現keyBackClicked和keyMenuClicked函數。然後調用CCDirector::sharedDirector()->getKeypadDispatcher()->addDelegate(pYourOwnHandler)。

Android上面,使用的本地方法是Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeKeyDown。有興趣可以自己研究。

對於字符串的輸入是一個比較複雜的話題。Cocos2Dx提供的功能不包括輸入法,雖然名字叫做IME。Cocos2Dx提供的處理字符輸入的功能包括:從系統接收輸入的字符串,然後進行分發;一些常見的輸入控件:CCTextFieldTTF和CCEditBox。Cocos2Dx的GUI部分還是比較弱,很多輸入控件的支持都需要自己去做。我們先看下用戶輸入的流程:

第一步:使得輸入控件得到焦點。在手機上一般就會彈出虛擬鍵盤。

第二步:用戶輸入內容。內容在控件上面得到及時地反應。

第三部:用戶輸入完畢。可以以回車結束,也可能是通過控件失去焦點。

第一步和第三步是用戶操作的主動過程。分別對應到CCIMEDelegate::attachWithIME和CCIMEDelegate::detachWithIME。第二步,每輸入一個字或者詞,操作系統就會將輸入的字符通過系統消息的方式告知應用。對應的接口是CCIMEDelegate::insertText和CCIMEDelegate::deleteBackward。

我們先看attachWithIME和detachWithIME。attachWithIME和detachWithIME在CCIMEDelegate中有默認實現。就是調用CCIMEDispatcher::attachDelegateWithIME和CCIMEDispatcher::detachDelegateWithIME。主要功能是在何時的時機調用CCIMEDelegate的四個函數:canAttachWithIME、didAttachWithIME、canDetachWithIME和didDetachWithIME。帶有can前綴的用來測試現在是否可以attach或者detach IME。did前綴的函數用來通知Delegate現在已經做了attach或detach IME。這些回調函數給Delegate一些處理其它的事務的機會。

但什麼怎麼觸發attachWithIME或者detachWithIME呢?答案是我們自己控制。舉一個常見例子:用戶點擊一個控件。控件在Cocos2Dx可以做成一個CCLayer。因爲CCLayer帶有處理觸控事件的能力。我們重載CCLayer::ccTouchEnd的函數,讓它收到控件所在區域的觸控消息後,就讓操作系統彈出鍵盤允許進行輸入,如果觸控區域不在控件所在區域,隱藏鍵盤。

1
2
3
4
5
6
7
8
9
10
11
12
void TextFieldTTFDefaultTest::onClickTrackNode(bool bClicked)
{
    CCTextFieldTTF * pTextField = (CCTextFieldTTF*)m_pTrackNode;
    if (bClicked)
    {
        pTextField->attachWithIME();
    }
    else
    {
        pTextField->detachWithIME();
    }
}

這裏用到了CCTextFieldTTF。他是Cocos2Dx提供的一個完整輸入控件。雖然還有CCEditBox,但它是以擴展插件的身份存在的,並且實現方式也不一樣。CCTextFieldTTF本身我們後面還會提到。

CCTextFieldTTF::attachWithIME和CCTextFieldTTF::detachWithIME內部會先調用CCIMEDelegate::attachWithIME和CCIMEDelegate::detachWithIME。因爲CCIMEDelegate::attachWithIME和CCIMEDelegate::detachWithIME會告知控件當前是否允許做IME的attach和detach操作。如果CCIMEDelegate的兩個函數返回fasle,那麼當前控件的IME attach和detach都會被拒絕。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
bool CCTextFieldTTF::attachWithIME()
{
    bool bRet = CCIMEDelegate::attachWithIME();
    if (bRet)
    {
        CCEGLView * pGlView = CCDirector::sharedDirector()->getOpenGLView();
        if (pGlView)
        {
            pGlView->setIMEKeyboardState(true);
        }
    }
    return bRet;
} bool CCTextFieldTTF::detachWithIME()
{
    bool bRet = CCIMEDelegate::detachWithIME();
    if (bRet)
    {
        CCEGLView * pGlView = CCDirector::sharedDirector()->getOpenGLView();
        if (pGlView)
        {
            pGlView->setIMEKeyboardState(false);
        }
    }
    return bRet;
}

CCTextFieldTTF::attachWithIME和CCTextFieldTTF::detachWithIME進一步讓操作系統準備輸入法。但這步依賴於Cocos2Dx所處的平臺,不同的平臺有不同的實現,Win32上面上面都不需要做,但Android上面就需要自己去通過InputMethodManager獲取輸入法服務。平臺的差異都隱藏在CCEGLView::setIMEKeyboardState後面。有興趣可以自己進一步深入跟進。

現在再來看CCIMEDelegate::insertText和CCIMEDelegate::deleteBackward。insertText代表我們現在正在進行輸入;deleteBackward代表我們現在需要刪除一個已經輸入的字符。CCIMEDelegate自身並沒有對這兩個函數提供默認的實現。可用的實現是CCTextFieldTTF。可用在CCTextFieldTTF進行自己的擴展。

看到上面的圖,可能會問CCIMEDelegate怎麼向CCIMEDispatcher進行註冊的呢?註釋說明了一下:CCIMEDelegate的構造函數會調用CCIMEDispatcher的單例對象的addDelegate函數註冊自己。addDelegate是保護成員,爲了能夠訪問它,聲明瞭CCIMEDelegate爲CCIMEDispatcher的友元。析構函數會做類似的取消註冊操作。CCTextFieldTTF繼承CCIMEDelegate,它構造或析構的時候,會分別調用CCIMEDelegate的構造和析構函數,CCTextFieldTTF也就自動向CCIMEDispatcher註冊和取消註冊。

CCTextFieldTTF的輸入字符顯示實際上是靠CCLabelTTFT完成的。CCLabelTTFT還有一個CCTextFieldDelegate成員,它將一些控制功能封裝在一個Delegate當中。CCTextFieldDelegate所有成員都返回bool值,決定相應的操作是否允許被執行。

現在走一些按鍵消息的分發流程。CCEGLView::WindowProc在收到鍵盤消息後,需要進行一下判斷:

  • 刪除鍵(VK_BACK),調用CCIMEDispatcher::dispatchDeleteBackward,進一步調用CCIMEDelegate::deleteBackward(子類實現)。

  • 回車鍵(VK_RETURN),調用CCIMEDispatcher::dispatchInsertText,進一步調用CCIMEDelegate::insertText(子類實現)。但並沒有直接送鍵碼,二是送了'\n'。

  • ASCII字符,同上。

  • 其他可輸入字符。在發送前需要做轉碼,寬字串轉UTF8。

CCIMEDelegate的子類CCTextFieldTTF收到按鍵輸入後,將其存放在一個字符串中。具體的實現,比較易懂,我們就不再累述。

另外,Android上面,字符輸入相關的代碼位於cocos2dx\platform\android\java\src\org\cocos2dx\lib\Cocos2dxTextInputWraper.java。

 

原文:http://my.oschina.net/sulliy/blog/289486

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章