關鍵字:焦點,Focus,加速鍵,Accelerator,OLEIVERB_UIACTIVATE,IHTMLWindow2,IHTMLDocument4 1、概述對於99%有UI的Windows應用程式來說,鍵盤操作都是不可或缺而又容易被人們遺忘的一環。如果對Windows組件作一次逐個的測試,我們會發現Microsoft提供的任何一個Windows組件都通過鍵盤實現完全的控制(“計算機”比較特殊,它是一個按鈕很多且每個按鈕都不能獲得焦點的程式,但在協助文檔中我們仍然可以找到為每個按鈕設定的快速鍵),這對於一個專業的Windows應用程式或軟體來說非常重要。換句話說,就算沒有滑鼠使用者也不應該束手無策,使用者應該可以通過鍵盤操作完成其希望的功能。焦點的轉移無疑是鍵盤操作的一個重要方面,在瀏覽器編程中尤其如此。 2、焦點的基本概念一般說來,在Windows中使用者通過鍵盤轉移焦點(Focus)有兩個方法:第一,對於輸入框附近有標籤提示的情況,按住Alt+某個預設的字母(Accelerator,加速鍵)將焦點快速轉移到輸入框。如所示,按下“Alt+D”,焦點應轉移到地址輸入框;按下“Alt+G”,焦點應轉移到搜尋方塊(本文對此不做討論)。第二,按住Tab鍵,焦點轉移到由應用程式控制的下一個可獲得焦點的視窗;按下Shift+Tab,焦點轉移到上一個可獲得焦點的視窗。如所示,如果地址輸入框是當前獲得焦點的視窗,則按下Tab時,焦點應轉移到搜尋方塊,再按下Shift+Tab,焦點應回到地址輸入框。
焦點的設定和轉移對於使用者體驗(Experience)來說是細微體貼而又重要的設計,但不幸的是不少Windows應用程式都或多或少犯了一些錯誤:
- 完全沒有加速鍵。
這在國產資訊系統中尤為常見。設計較差的資訊系統常常會出現一個視窗擁有數十個輸入框的情況,如果為每個編輯框都提供一個加速鍵的話,問題就出來了。字母鍵只有26個,就算把數字鍵也用上,也難免不能滿足要求,所以很多資訊系統乾脆就不要加速鍵。
- 擺設用的加速鍵。
一些應用軟體甚至不懂得加速鍵的意義,只知道依樣畫葫地在輸入框的旁邊用標籤說明加速鍵,但僅此而已,使用者根本無法通過Alt+加速鍵轉移焦點到輸入框。
- 錯誤地(或不能)轉移焦點
對於基於對話方塊的應用程式來說,常犯的錯誤是使用者按下Tab鍵時,焦點出乎使用者意料地在輸入框之間亂竄。而在這樣的例子中,常犯的錯誤則是不能通過Tab轉移焦點,或者按Tab能轉移焦點但按Shift+Tab不能朝反方向轉移焦點。
- 對嵌入的ActiveX控制項缺乏處理
對於嵌入的ActiveX控制項,尤其是WebBrowser控制項來說,焦點的處理就更為麻煩了(這本是基於WebBrowser的瀏覽器編程的難題之一)。常見的瀏覽器要麼不處理常規視窗與WebBrowser控制項之間的焦點傳遞(Maxthon、Gosurf只支援在輸入框之間傳遞焦點);要麼處理不完整,焦點一旦從某個輸入框轉移到WebBrowser控制項就再也回不來(如GreenBrowser);更有的根本就不處理任何焦點的傳遞(如世界之窗瀏覽器)。 按照本系列文章的慣例,本文討論的目的將是提供一個完整(未必完美)的解決方案——:一,焦點在嵌入ReBar的各個輸入框之間傳遞;二,焦點在普通Windows視窗(輸入框)與WebBrowser控制項之間傳遞。 3、設定目標說明了我們希望實現的正常的焦點轉移行為:
- 從工具條上的任何一個輸入框出發,按Tab將焦點轉移到下一個輸入框,按Shift+Tab將焦點轉移到上一個輸入框
- 如果焦點所在輸入框是工具條上的最後一個輸入框,按Tab將焦點轉移到WebBrowser控制項當前的活動Html Element(上一次獲得焦點的Element)
- 如果焦點所在輸入框是工具條上的第一個輸入框,按Shift+Tab將焦點轉移到WebBrowser控制項當前活動Html Element
- 對於上面兩種情況,若WebBrowser控制項沒有當前活動的可獲得焦點Html Element,則焦點應從輸入框轉移到WebBrowser控制項的第一個或最後一個可獲得焦點的Html Element
- 如果焦點當前位於WebBrowser控制項中,按Tab將焦點轉移到下一個Html Element,按Shift+Tab將焦點轉移到上一個Html Element
- 如果焦點當前位於WebBrowser控制項中,且當前的活動Html Element是最後一個可獲得焦點的Html Element,按Tab將焦點轉移到工具條的第一個輸入框
- 如果焦點當前位於WebBrowser控制項中,且當前的活動Html Element是第一個可獲得焦點的Html Element,按Shif+Tab將焦點轉移到工具條的最後輸入框
以為例,“Google大全”為WebBrowser當前獲得焦點的Html Element,舉例如下:
- 例1:假設當前焦點位於地址輸入框,按下Tab鍵不鬆開,焦點轉移的順序應是:“地址欄”,“搜尋欄”,“Google大全”……“將Google設為首頁”,“地址欄”,“搜尋欄”,“個人化首頁”,“搜尋記錄”……
- 例2:假設當前焦點位於地址輸入框,且WebBrowser控制項沒有活動的獲得焦點的Html Element,按下Tab鍵不鬆開,焦點轉移的順序應是:“地址欄”,“搜尋欄”,“個人化首頁”,“搜尋記錄”……“將Google設為首頁”,“地址欄”,……
- 例3:假設當前焦點位於“搜尋記錄”,按下Shift+Tab鍵不鬆開,焦點轉移的順序應是:“搜尋記錄”,“個人化首頁”,“搜尋欄”,“地址欄”,“將Google設為首頁”……“搜尋記錄”……
4、工具條輸入框之間的焦點轉移為實現統一的處理,我們從CDialogBar派生一個CDialogBarEx類,由該類處理Tab/Shift Tab按鍵,而輸入框(如EditBox,ComboBox等)則放在CDialogBarEx的衍生類別(如CUrlAddressBar、CSearchBar等)中,這樣輸入框就可以專註於其它的功能。範例程式碼如下:
BOOL CDialogBarEx::PreTranslateMessage(MSG* pMsg){
if ( ( pMsg->message==WM_KEYDOWN ) ){
if ( (pMsg->wParam == VK_TAB) ){//由MainFrame處理如何轉移焦點,按下Shift表示焦點應轉移到上一個視窗g_pMainFrame->SetFocusToNextControl( GetKeyState(VK_SHIFT) >= 0 );return TRUE;}}......
return CDialogBar::PreTranslateMessage(pMsg);}
void CMainFrame::SetFocusToNextControl(
bool bNext){//m_wndReBar是一個CReBarEx,可從CReBar派生if ( !m_wndReBar.SetFocusToNextControl(bNext) ){//如果CReBarEx在其子視窗中找不到下(上)一個可以設定焦點的視窗,則把焦點轉移到WebBrowserCChildFrame *pChildFrame = (CChildFrame *)MDIGetActive();
if ( pChildFrame && pChildFrame->GetActiveView() ) {pChildFrame->GetActiveView()->SetFocus();}}}
bool CReBarEx::SetFocusToNextControl(
bool bNext){
return bNext ? FocusNextControl() : FocusPrevControl();}
bool CReBarEx::FocusNextControl(){REBARBANDINFO rbbi;rbbi.cbSize = sizeof( rbbi );rbbi.fMask = RBBIM_CHILD; //先找到當前獲得焦點的BandUINT nBand;
for ( nBand = 0; nBand < m_rbCtrl.GetBandCount(); nBand++ ){VERIFY( m_rbCtrl.GetBandInfo(nBand, &rbbi) );if ( ::IsChild(rbbi.hwndChild, ::GetFocus()) ){break;}} //如果運行到這裡,必定能夠找到當前獲得焦點的BandASSERT(nBand < m_rbCtrl.GetBandCount());
for ( nBand = nBand + 1; nBand < m_rbCtrl.GetBandCount(); nBand++ ){VERIFY( m_rbCtrl.GetBandInfo(nBand, &rbbi) );::SetFocus(rbbi.hwndChild);
if ( ::IsChild(rbbi.hwndChild, ::GetFocus()) ){//成功找到並設定焦點到下一個視窗
return
true;}}//當前獲得焦點的視窗已經是ReBarEx中最後一個可獲得焦點的視窗
return
false;} bool CReBarEx::FocusPrevControl(){//實現與FocusNextControl類似,此處略去}
void CReBarEx::OnSetFocus(CWnd* pOldWnd){//如果此時Shift為按下的狀態,表示焦點可能是從WebBrowser的第一個活動Html Element轉過來,//則將焦點轉移到最後一個輸入框,否則轉移到第一個輸入框//SetFocusToLastControl與SetFocusToFirstControl的實現相當簡單,略去
return GetKeyState(VK_SHIFT) < 0 ? SetFocusToLastControl() : SetFocusToFirstControl();}
5、焦點從WebBrowser轉移到工具條輸入框處理瀏覽器的按鍵也曾是嵌入WebBrowser控制項的編程難題之一,Delphi對WebBrowser的封裝對按鍵的支援就存在很大問題。在《Programming Internet Explorer》中曾提到的方法是處理MainFrame的PreTranslateMessage,並在其中從WebBrowser的Document查詢得到IOleInPlaceActiveObject介面,將按鍵交給IOleInPlaceActiveObject的TranslateAccelerator成員區處理。查詢MSDN我們可以知道,IOleInPlaceActiveObject::TranslateAccelerator被調用時,MSHTML引擎會調用IDocHostUIHandler介面的TranslateAccelerator方法,從而給開發人員一個介面來處理按鍵。所以對於實現了IDocHostUIHandler介面的應用程式來說,按鍵處理就非常簡單了。
//在此處理將焦點從WebBrowser中轉移到ReBar上的輸入框HRESULT CMyView::OnTranslateAccelerator(LPMSG lpMsg,const GUID* pguidCmdGroup, DWORD nCmdID){
if (lpMsg && lpMsg->message == WM_KEYDOWN && lpMsg->wParam == VK_TAB){LPDISPATCH lpDispatch = GetHtmlDocument();CComQIPtr<IHTMLDocument2> pHTMLDoc = lpDispatch;
if ( pHTMLDoc ){CComQIPtr<IHTMLElement> pElement;
if ( SUCCEEDED(pHTMLDoc->get_activeElement(&pElement)) && !pElement ){//沒有任何活動的Html Element,把焦點轉移到ReBarg_pMainFrame->m_wndReBar.SetFocus();//通知MSHTML不要再繼續處理按鍵
return S_OK;}}}
return S_FALSE;}
6、使WebBrowser獲得焦點使瀏覽器獲得焦點也頗為講究。我的一篇老文章《TWebBrowser編程簡述中》寫到有好幾種方法可以使WebBrowser獲得焦點:IOleObject::DoVerb(OLEIVERB_UIACTIVATE...)、IHTMLWindow2::focus()、IHTMLDocument4::focus()。而實際上這幾種方法是有區別的(內部實現我們並不清楚,也不關心)。
- IOleObject::DoVerb能夠將焦點設定到WebBrowser上一次失去焦點時獲得焦點的Html Element上。缺點在於如果WebBrowser上次失去焦點時沒有任何Html Element獲得焦點,則DoVerb並不能保證焦點會轉移到WebBrowser中。
- IHTMLWindow2::focus不管三七二十一,將焦點轉移到WebBrowser的開頭Html Element。這顯然不是我們想要的。
- 測試的結果,IHTMLDocument4::focus似乎能夠滿足要求:能夠記住WebBrowser上次失去焦點時獲得焦點的Html Element;在WebBrowser上次失去焦點時沒有任何Html Element獲得焦點的情況下,能夠焦點轉移到開頭的Html Element。但事實上並不理想,假如按住Tab鍵不鬆開,反覆調用IHTMLDocument4::focus多次之後,我們會發現焦點再也到不到WebBrowser中了。
有沒有完美解決的辦法呢?答案當然是Positive的,如下:
void CMyView::OnSetFocus(CWnd* pOldWnd){LPDISPATCH lpDisp = GetHtmlDocument();CComQIPtr<IHTMLDocument2, &IID_IHTMLDocument2> pHTMLDoc(lpDisp);
if ( pHTMLDoc ){CComQIPtr<IHTMLElement> pElement;
if ( SUCCEEDED(pHTMLDoc->get_activeElement(&pElement)) && !pElement ){//沒有任何使用中的元素,把焦點轉移到WebBrowser的開頭CComQIPtr<IHTMLWindow2> pHTMLWnd;
if( SUCCEEDED(pHTMLDoc->get_parentWindow( &pHTMLWnd )) && pHTMLWnd ){pHTMLWnd->focus();
return;}}} //有活動的元素(上一次的焦點),直接將焦點轉移過去//CWnd::SetFocus()會調用IOleObject::DoVerb()正確地設定焦點m_wndBrowser.SetFocus();}
7、總結至此,我們就算完整地實現了焦點在普通視窗和瀏覽器之間的傳遞,任何時候,按住Tab鍵不鬆開,焦點將會在所有可獲得焦點的視窗之間迴圈傳遞;同樣,按住Shift-Tab不鬆開,焦點會以反方向傳遞。而不會出現使用者無法將焦點轉移到瀏覽器視窗的情況,或者焦點無法從瀏覽器視窗轉移到輸入框的情況。當然,還有比較重要也比較抽象的一點,增強了使用者體驗,呵呵。 8、參考資料《Programming Internet Explorer》
引用地址: Internet Explorer 編程簡述(十二)正確地設定和轉移焦點