Internet Explorer 編程簡述(十一)實現完美的Inplace Drag & Drop——“超級拖放”

來源:互聯網
上載者:User
關鍵字:超級拖放,GetDropTarget,ondragover,IHTMLDataTransfer 1、概述許多多視窗瀏覽器都提供了一種被稱為“超級拖放”(或“超級拖拽”、“隨心拖放”等等,不一而足)的功能。作為對IE拖拽行為對擴充,“超級拖放”實現了一些非常實用的功能:
  • 拖放網頁連結:通常是在新視窗中開啟
  • 拖放選中的文字:儲存文字、作為關鍵字通過搜尋引擎搜尋網路、作為Url開啟等
  • 拖放圖片:通常是儲存圖片到指定檔案夾
  • 當然,還有很關鍵的一點:拖動對象時滑鼠指標反饋不同的拖拽效果
在《Internet Explorer 編程簡述(十)響應來自HTML Element的事件通知——幾個好用的類》中曾提到,儘管許多瀏覽器都提供了超級拖放的功能,但與IE的預設實現相比,除了具備滑鼠指標拖拽效果外,還沒有哪個瀏覽器的實現能夠實現:
  • 文字在頁面內與輸入框之間的互動拖放(這一點最為重要)
  • 來自外部的文字與網頁輸入框之間的互動拖放
  • 拖拽時滾動頁面(這一點是被忽略了)
 本文的目的,一是介紹實現超級拖放的兩種方法,二是說明如何?“完美”的拖放——即擴充IE拖拽行為的同時,保留IE預設的拖拽行為。三是給出一個最為直接和簡潔的實現,至於拖放不同的對象以實現不同的功能,不在本文討論的範圍,略去。  2、標準的實現方法標準方法即通過IDocHostUIHandler的GetDropTarget成員函數來實現,在MSDN這樣說到:
IDocHostUIHandler::GetDropTarget Method——Called by MSHTML when it is used as a drop target. This method enables the host to supply an alternative IDropTarget interface.
即在適當的時候,MSHTML引擎會調用IDocHostUIHandler的GetDropTarget方法,為應用程式提供一個機會來替換MSHTML預設的DropTarget實現。我們就可以通過這個自訂的DropTarget實現來完成上述的“超級拖放”功能。方法樣本如下,其中略去的部分可參考MFC中CHtmlControlSite和CHtmlView的原始碼: 
STDMETHODIMP CHtmlControlSite::XDocHostUIHandler::GetDropTarget(LPDROPTARGET pDropTarget, LPDROPTARGET* ppDropTarget){METHOD_PROLOGUE_EX_(CHtmlControlSite, DocHostUIHandler)*ppDropTarget = g_pDropTarget;//將自訂的實現告知MSHTML引擎 return S_OK;}
 其中g_pDropTarget指向某個全域的IDropTarget介面的實現,我們假定為CIEDropTarget,CIEDropTarget實現了IDropTarget的幾個成員函數DragEnter、DragOver、DragLeave和Drop。在DragEnter中可以決定是否接受一個Drop以及如果接受這個Drop的話該提供怎樣的滑鼠拖拽反饋,在持續觸發的DragOver中同樣可以設定滑鼠拖拽反饋,從而實現在拖放不同的對象(文字、連結、映像等)時提供不同的拖拽視覺效果,實現相當簡單,此處不再贅述。但上面的實現存在一些問題。首先是選中的文字在頁面內與輸入框之間互動的拖放沒有了。這是自然的,既然我們用自訂的DropTarget替換掉了IE的預設實現,那這種互動的拖放理應由我們自己實現。難處並非在於不能實現,而是在於實現起來比較麻煩——光是得到滑鼠下的HTML Element就夠我們煩了;當輸入框中有文字的時候,游標還應該隨著滑鼠的移動而移動——所以這個費力還不一定討好的功能似乎沒有哪個瀏覽器去做。其次,作為輸入框文字拖放的衍生物,拖拽滾動沒有了。當滑鼠向某個方向拖拽時,網頁應該隨著將不可見的部分滾動出來,比如某個輸入框,讓我們有機會將文字拖拽過去。這個Feature的實現並不困難,不過一來是被忽略了(注意到拖拽滾動的人並不多),二來主要Feature都沒有實現,這個滾動也意義不大了。 3、打入MSHTML內部既然從GetDropTarget提供外部實現難以得到與輸入框的互動式拖放,那就換個角度來考慮問題,讓我們打入MSHTML的內部。著手點是IHTMLDocumentX介面——操縱IE的DOM的法寶。我們注意到IHTMLDocument2有個ondragstart事件,進而想到應該也有諸如ondragenter、ondragover、ondrop之類的事件(事實上也是有的),如果響應這些事件,處理同輸入框的互動式拖放應該就能夠解決。因為這些拖放在MSHTML的預設DropTarget實現中發生,因而當滑鼠拖拽到某個輸入框上時,肯定會觸發一個ondragover事件,而在IHTMLEventObj的輔助下我們能輕鬆得到相關的HTML Element,其它的操作就容易進行了。再細心一點,我們還發現IHTMLEventObj2介面有個dataTransfer屬性——可以得到一個IHTMLDataTransfer的指標,而IHTMLDataTransfer介面正是瀏覽器內部用於資料交換的重要手段之一(看看它的屬性就知道會很有用了):
IHTMLDataTransfer MembersclearData——Removes one or more data formats from the clipboard through dataTransfer or clipboardData object.dropEffect——Sets or retrieves the type of drag-and-drop operation and the type of cursor to display.effectAllowed——Sets or retrieves, on the source element, which data transfer operations are allowed for the object.getData——Retrieves the data in the specified format from the clipboard through the dataTransfer or clipboardData objects.setData——Assigns data in a specified format to the dataTransfer or clipboardData object.
 更進一步,從IHTMLDataTransfer介面還可以訪問到IDataObject介面,在進行Ole拖放時,資料就是通過IDataObject介面來傳遞的。具體用法稍後討論。 4、打入MSHTML內部——思路提供滑鼠反饋效果與實現GetDropTarget的方法類似,有了IHTMLDataTransfer介面,便可在ondragstart及ondragover事件觸發時通過dropEffect屬性設定拖拽的效果(可根據需要自行設定,不設定的話使用預設的效果)。再者,“拖”和“放”都在MSHTML的預設實現中發生,我們從IHTMLEventObj的SrcElement即可得知滑鼠所位置的HTML Element是否是輸入框。 5、打入MSHTML內部——實現要接收到ondragstart之類的事件,可以採用《Internet Explorer 編程簡述(十)響應來自HTML Element的事件通知——幾個好用的類》中提到的CHtmlObj類和CHtmlElements類,並在適當的地方串連到Document,範例程式碼如下所示: 
HRESULT CHtmlDocument2::OnInvoke(DISPID dispidMember, REFIID riid, LCID lcid, WORD wFlags,DISPPARAMS * pdispparams, VARIANT * pvarResult,EXCEPINFO * pexcepinfo, UINT * puArgErr){......//如果只是要設定滑鼠拖拽效果的話,這個事件可以不處理 case DISPID_HTMLELEMENTEVENTS_ONDRAGSTART :{OnDragStart(); break;}//重點在這裡 case DISPID_HTMLELEMENTEVENTS_ONDRAGOVER :{OnDragOver(); break;} case DISPID_HTMLELEMENTEVENTS_ONDROP :{OnDrop(); break;}......}  void CHtmlDocument2::OnDragOver( void){SetDragEffect();               //設定滑鼠拖拽效果}  void CHtmlDocument2::SetDragEffect( void){CComQIPtr<IHTMLWindow2>  pWindow;CComQIPtr<IHTMLEventObj>  pEventObj;CComQIPtr<IHTMLEventObj2>  pEventObj2;CComQIPtr<IHTMLElement>  pElement; HRESULT hr = m_spHtmlObj->get_parentWindow( &pWindow );hr = pWindow->get_event( &pEventObj ); //ondragover發生時IE的預設行為是“沒有滑鼠拖拽效果”。//將IHTMLEventObj的傳回值設為false即可取消該事件的預設行為,所以執行完下面這句話,拖拽效果就出現了。AllowDisplayDragCursor(pEventObj, FALSE);    CComBSTR bstrTagName;pEventObj->get_srcElement(&pElement);    //獲得當前HTML ElementpElement->get_tagName(&bstrTagName);     if ( IsEditArea(bstrTagName) ) //根據Tag Name判斷是否滑鼠位於輸入框,以便設定焦點使得游標隨滑鼠移動{CComQIPtr<IHTMLElement2>  pElement2; if ( SUCCEEDED(pElement->QueryInterface(IID_IHTMLElement2, (void **) &pElement2 )) && pElement2 ){pElement2->focus();}//預設情況下,當拖拽文檔到輸入框時,滑鼠會變成拖拽的游標,所以這裡使用IE的預設行為。AllowDisplayDragCursor(pEventObj, TRUE);}} BOOL CHtmlDocument2::IsEditArea(CComBSTR bstrTagName){ return bstrTagName == "INPUT" || bstrTagName == "TEXTAREA";}  void CHtmlDocument2::AllowDisplayDragCursor(CComQIPtr<IHTMLEventObj> pEventObj, BOOL bAllow){VARIANT v;v.vt = VT_BOOL; v.boolVal = !bAllow ? VARIANT_FALSE : VARIANT_TRUE;pEventObj->put_returnValue(v);}  void CHtmlDocument2::OnDrop( void){CComQIPtr<IHTMLWindow2>  pWindow;CComQIPtr<IHTMLEventObj>  pEventObj;CComQIPtr<IHTMLEventObj2>  pEventObj2;CComQIPtr<IHTMLElement>  pElement;CComQIPtr<IHTMLDataTransfer>   pdt; //此處示範如何使用IHTMLDataTransfer HRESULT hr = m_spHtmlObj->get_parentWindow( &pWindow );hr = pWindow->get_event( &pEventObj );hr = pEventObj->QueryInterface(IID_IHTMLEventObj2, (void **) &pEventObj2 );hr = pEventObj2->get_dataTransfer(&pdt); CComBSTR bstrFormat = "URL"; //首先嘗試擷取URLVARIANT Data;hr = pdt->getData(bstrFormat, &Data); if ( Data.vt != VT_NULL ){     //擷取成功,拖放的對象是UrlDoOpenUrl(CString(Data.bstrVal));}else{     //否則嘗試擷取選中的文本bstrFormat = "Text";hr = pdt->getData(bstrFormat, &Data); if ( Data.vt != VT_NULL ){     //擷取成功,拖放的內容是文本CComBSTR bstrTagName;pEventObj->get_srcElement(&pElement);pElement->get_tagName(&bstrTagName); if ( IsEditArea(bstrTagName) ){//Drop target是輸入框,不做任何操作,由IE進行預設處理 return;} else{     //否則我們自己處理文本,或儲存,或檢測是否連結後開啟,等等DoProcessText(CString(Data.bstrVal));//Process the text}} else{     //既不是連結,也不是文本,可認為是來自外部(如Windows Shell)的檔案拖放DoOnDropFiles(pdt);}}} //示範如何從IHTMLDataTransfer得到IDataObject

void CHtmlDocument2::DoOnDropFiles(CComQIPtr<IHTMLDataTransfer> pDataTransfer)

{CComQIPtr<IServiceProvider>  psp;CComQIPtr<IDataObject>  pdo;if ( FAILED(pDataTransfer->QueryInterface(IID_IServiceProvider, (void **) &psp)) ){return;}if ( FAILED(psp->QueryService(IID_IDataObject, IID_IDataObject, (void **) &pdo)) ){return;} COleDataObject DataObject;DataObject.Attach(pdo);......}

 6、再次回到標準方法上述通過Event Sink響應網頁拖拽的方法已經能夠很好地工作,可說“趨於完美”了,但仍有兩個“小”問題:第一,必須與document建立串連才能工作,而建立串連的時機不容易掌握(MSDN中推薦的位置是DocumentComplete,但在NavigateComplete中也可,或者是檢測到WebBrowser的readystate變為READYSTATE_INTERACTIVE時進行串連)。第二,實現方法還是略顯複雜。有沒有更簡單的方法呢?我決定再次對GetDropTarget進行“調研”。所謂“踏破鐵鞋無覓處,得來全不費功夫”,晃了一眼GetDropTarget方法的聲明後,靈機一動,我忽然想到了辦法。事實證明,這是完美的解決辦法。 讓我們再來看看GetDropTarget的聲明,其中第一個參數指向MSHTML提供的預設DropTarget實現,而第二個參數用以返回應用程式的自訂DropTarget實現,如果在GetDropTarget中返回S_OK,MSHTML將以應用程式提供的自訂DropTarget替換預設的DropTarget實現。

HRESULT GetDropTarget( IDropTarget *pDropTarget, IDropTarget **ppDropTarget);

參數說明

pDropTarget

[in] Pointer to an IDropTarget interface for the current drop target object supplied by MSHTML.

ppDropTarget

[out] Address of a pointer variable that receives an IDropTarget interface pointer for the alternative drop target object supplied by the host.

想到了嗎?解決問題的關鍵就在於第一個參數pDropTarget。相信很多瀏覽器在處理的時候都忽略掉了第一個參數而只是將自己的實現通過第二個參數告知MSHTML,因而丟失了IE預設的行為。既然如此,將預設的IDropTarget介面的指標儲存下來,在適當的時候調用,不就能夠保留IE的原始拖允許存取為了嗎?

 7、完美實現

完整的代碼就不再給出,我們只列出關鍵的部分作為樣本。假設我們用來實現IDropTarget介面的類叫做CBrowserDropTarget:

//建構函式,傳入參數即是從GetDropTarget得到的那個pDropTarget,它是MSHTML的預設實現CBrowserDropTarget::CBrowserDropTarget(IDropTarget *pOrginalDropTarget):  m_bDragTextToInputBox(FALSE)//這個布爾變數用來判斷是否正在向InputBox拖拽文字,  m_pOrginalDropTarget(pOrginalDropTarget)//m_pOrginalDropTarget用來儲存MSHTML的預設實現{} STDMETHODIMP CBrowserDropTarget::DragEnter(/* [unique][in] */IDataObject __RPC_FAR *pDataObj,/* [in] */ DWORD grfKeyState,/* [in] */ POINTL pt,/* [out][in] */ DWORD __RPC_FAR *pdwEffect){//調用預設的行為 return m_pOrginalDropTarget->DragEnter(pDataObj, grfKeyState, pt, pdwEffect);} STDMETHODIMP CBrowserDropTarget::DragOver(/* [in] */ DWORD grfKeyState,/* [in] */ POINTL pt,/* [out][in] */ DWORD __RPC_FAR *pdwEffect){//在網頁內拖拽文字時這個值是DROPEFFECT_COPY(拖拽的文字不屬於輸入框中)//或DROPEFFECT_COPY | DROPEFFECT_MOVE(拖拽的文字是輸入框中的文字)DWORD dwTempEffect = *pdwEffect; //接下來調用IE的預設行為HRESULT hr = m_pOrginalDropTarget->DragOver(grfKeyState, pt, pdwEffect); //判斷是否是往輸入框拖拽文字m_bDragTextToInputBox = IsDragTextToInputBox(dwOldEffect, *pdwEffect); if ( !m_bDragTextToInputBox ){//不是往輸入框拖拽文字,則使用原始的拖拽效果。否則和IE的預設效果一樣——也就是沒有效果*pdwEffect = dwTempEffect;} return S_OK;} //根據調用預設行為前後的Effect值判斷是否是往輸入框拖拽文字BOOL CBrowserDropTarget::IsDragTextToInputBox(DWORD dwOldEffect, DWORD dwNewEffect){//如果是把非輸入框中文字往輸入框拖動,則dwOldEffect與dwNewEffect相等,都是DROPEFFECT_COPYBOOL bTextSelectionToInputBox = ( dwOldEffect == DROPEFFECT_COPY )&& ( dwOldEffect == dwNewEffect ); //如果是把文字從一個輸入框拖到另一個輸入框,則dwOldEffect為DROPEFFECT_COPY | DROPEFFECT_MOVE,//而dwNewEffect的值可能為DROPEFFECT_MOVE(預設情況),也可能為DROPEFFECT_COPY(按下Ctrl鍵時)BOOL bInputBoxToInputBox = ( dwOldEffect == (DROPEFFECT_COPY | DROPEFFECT_MOVE) )&& ( dwNewEffect == DROPEFFECT_MOVE || dwNewEffect == DROPEFFECT_COPY ); //來自Microsoft Word的拖拽特殊一些,dwOldEffect是所有效果的組合值BOOL bMSWordToInputBox = ( dwOldEffect == (DROPEFFECT_COPY | DROPEFFECT_MOVE | DROPEFFECT_LINK) )&& ( dwNewEffect == DROPEFFECT_MOVE || dwNewEffect == DROPEFFECT_COPY ); //來自Edit Plus的拖拽過也特殊一些,dwOldEffect是個負數(懷疑是Edit Plus的拖拽實現有問題)BOOL bEditPlusToInputBox = ( dwOldEffect < 0 )&& ( dwNewEffect == DROPEFFECT_MOVE || dwNewEffect == DROPEFFECT_COPY ); //也許還有些例外,可再添加...... return bTextSelectionToInputBox || bInputBoxToInputBox || bMSWordToInputBox || bEditPlusToInputBox;} STDMETHODIMP CBrowserDropTarget::DragLeave(){//調用預設的行為 return m_pOrginalDropTarget->DragLeave();} STDMETHODIMP CBrowserDropTarget::Drop(/* [unique][in] */ IDataObject __RPC_FAR *pDataObj,/* [in] */ DWORD grfKeyState,/* [in] */ POINTL pt,/* [out][in] */ DWORD __RPC_FAR *pdwEffect){ if ( m_bDragTextToInputBox ){//是文字拖放,調用IE的預設行為return m_pOrginalDropTarget->Drop(pDataObj, grfKeyState, pt, pdwEffect);} //否則是拖放連結、圖片、檔案等,按常規的IDataObject處理方式...... return S_OK;}
 至此,我們就得到了一個完美的“超級拖放”的基本架構,它在擴充的同時保留了IE的預設行為:
  1. 文字在頁面內與輸入框之間能夠互動拖放。
  2. 來自外部的文字與網頁輸入框之間也能互動拖放
  3. 拖拽時能夠自動滾動頁面
 其餘的功能,如向不同的方向拖拽以完成不同的工作,左鍵右鍵拖放執行不同的功能,按住Alt儲存文字等等,可根據需要自行實現,不再討論。

8、修正今天和Stanley Xu聊了幾個鐘頭,受益匪淺。根據Stanley的提議,毋須再作是否往輸入框拖拽文字的判斷,因為我們需要的只是在IE的預設行為沒有滑鼠拖拽效果的時候讓它有拖拽效果,因此只需要簡單地判斷調用IE預設行為後的Effect值是否為0即可,如下://判斷是否是往輸入框拖拽文字m_bDragTextToInputBox = *pdwEffect != 0;簡單而直接,當然更重要的是:可用。

 9、參考資料MSDN: IHTMLEventObj InterfaceMSDN: IHTMLDataTransfer Interface《Internet Explorer 編程簡述(十)響應來自HTML Element的事件通知——幾個好用的類》 

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.