cef osr拖拽功能实现

Posted Redrain

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了cef osr拖拽功能实现相关的知识,希望对你有一定的参考价值。

转载请说明原出处,谢谢~~:https://redrain.blog.csdn.net/article/details/107105312

cef显示web分为窗口模式和离屏渲染模式(osr,off screen rendering)。窗口模式使用起来比较简单,基本的功能都已经实现,包括web内部的拖拽。而osr模式需要实现相关接口比较麻烦

窗口模式:

窗口模式的拖拽控制接口只需要关心CefDragHandler。

class CefDragHandler : public virtual CefBaseRefCounted 
 public:
  typedef cef_drag_operations_mask_t DragOperationsMask;

  ///
  // Called when an external drag event enters the browser window. |dragData|
  // contains the drag event data and |mask| represents the type of drag
  // operation. Return false for default drag handling behavior or true to
  // cancel the drag event.
  ///
  /*--cef()--*/
  virtual bool OnDragEnter(CefRefPtr<CefBrowser> browser,
                           CefRefPtr<CefDragData> dragData,
                           DragOperationsMask mask) 
    return false;
  

  ///
  // Called whenever draggable regions for the browser window change. These can
  // be specified using the '-webkit-app-region: drag/no-drag' CSS-property. If
  // draggable regions are never defined in a document this method will also
  // never be called. If the last draggable region is removed from a document
  // this method will be called with an empty vector.
  ///
  /*--cef()--*/
  virtual void OnDraggableRegionsChanged(
      CefRefPtr<CefBrowser> browser,
      const std::vector<CefDraggableRegion>& regions) 
;

其中CefDragHandler::OnDragEnter在web中有内容被拖拽时被调用,这时可以根据拖拽的内容,决定是否要阻止拖拽。

CefDragHandler::OnDraggableRegionsChanged是让web内部自己设置一个拖拽区域,然后通知给c++,让c++把这块区域也设置为非客户区,用户可以拖拽这块区域来移动整个窗口

 

osr模式:

离屏渲染模式需要自己实现拖拽接口,离屏渲染继承了CefRenderHandler接口,其中有两个方法是实现拖拽的:

  // Called when the user starts dragging content in the web view. Contextual
  // information about the dragged content is supplied by |drag_data|.
  // (|x|, |y|) is the drag start location in screen coordinates.
  // OS APIs that run a system message loop may be used within the
  // StartDragging call.
  //
  // Return false to abort the drag operation. Don't call any of
  // CefBrowserHost::DragSource*Ended* methods after returning false.
  //
  // Return true to handle the drag operation. Call
  // CefBrowserHost::DragSourceEndedAt and DragSourceSystemDragEnded either
  // synchronously or asynchronously to inform the web view that the drag
  // operation has ended.
  ///
  /*--cef()--*/
  virtual bool StartDragging(CefRefPtr<CefBrowser> browser,
                             CefRefPtr<CefDragData> drag_data,
                             DragOperationsMask allowed_ops,
                             int x,
                             int y) 
    return false;
  

  ///
  // Called when the web view wants to update the mouse cursor during a
  // drag & drop operation. |operation| describes the allowed operation
  // (none, move, copy, link).
  ///
  /*--cef()--*/
  virtual void UpdateDragCursor(CefRefPtr<CefBrowser> browser,
                                DragOperation operation) 

其中StartDragging方法是web开始拖拽时的回调,在这里可以按照windows系统的拖拽模块来实现一个阻塞的拖拽功能。参照cef demo的写法,把osr_dragdrop_win.h、osr_dragdrop_win.cc、osr_dragdrop_events.h这三个文件搬过来,里面实现了windows的拖拽需要的DropTargetWin类。把cef demo的代码搬过来填充到StartDragging里。

为了让DropTargetWin可以正常工作,需要实现osr_dragdrop_events.h中的OsrDragEvents接口。

除了这些工作,就是windows窗口需要实现拖拽功能,需要调用一个api RegisterDragDrop,这个api让窗口的拖拽事件与DropTargetWin关联,当窗口收到拖拽相关消息时会通知DropTargetWin,DropTargetWin再去调用browser中对应一些接口来通知web进行拖拽响应。

理论上实现完这些步骤就可以完成拖拽了。具体的实现代码可以参考cef client demo。

 

我遇到的坑:

我的osr模式的拖拽实现完毕后,出现了一个奇怪的问题:

  1. 某些网页中被拖拽的内容松开后,会托拽失败,回到原位
  2. 某些网页中被拖拽的内容松开后,就会执行网页的跳转操作

刚碰到这个问题,从现象来看,我以为是osr模式中一些鼠标坐标处理有问题,调试了2天也没发现问题。与cef demo反复对比也没发现什么差异。最终看StartDragging方法的描述时注意到一点:

  ///
  // Called when the user starts dragging content in the web view. Contextual
  // information about the dragged content is supplied by |drag_data|.
  // (|x|, |y|) is the drag start location in screen coordinates.
  // OS APIs that run a system message loop may be used within the
  // StartDragging call.
  //
  // Return false to abort the drag operation. Don't call any of
  // CefBrowserHost::DragSource*Ended* methods after returning false.
  //
  // Return true to handle the drag operation. Call
  // CefBrowserHost::DragSourceEndedAt and DragSourceSystemDragEnded either
  // synchronously or asynchronously to inform the web view that the drag
  // operation has ended.
  ///
  /*--cef()--*/
  virtual bool StartDragging(CefRefPtr<CefBrowser> browser,
                             CefRefPtr<CefDragData> drag_data,
                             DragOperationsMask allowed_ops,
                             int x,
                             int y);

文档最后说到在拖拽操作完成后,需要同步异步的调用DragSourceEndedAtDragSourceSystemDragEnded方法来通知拖拽接口。我在StartDragging中的确同步调用了这两个方法,然后继续看这两个方法的文档:

  ///
  // Call this method when the drag operation started by a
  // CefRenderHandler::StartDragging call has ended either in a drop or
  // by being cancelled. |x| and |y| are mouse coordinates relative to the
  // upper-left corner of the view. If the web view is both the drag source
  // and the drag target then all DragTarget* methods should be called before
  // DragSource* mthods.
  // This method is only used when window rendering is disabled.
  ///
  /*--cef()--*/
  virtual void DragSourceEndedAt(int x, int y, DragOperationsMask op) = 0;

  ///
  // Call this method when the drag operation started by a
  // CefRenderHandler::StartDragging call has completed. This method may be
  // called immediately without first calling DragSourceEndedAt to cancel a
  // drag operation. If the web view is both the drag source and the drag
  // target then all DragTarget* methods should be called before DragSource*
  // mthods.
  // This method is only used when window rendering is disabled.
  ///
  /*--cef()--*/
  virtual void DragSourceSystemDragEnded() = 0;

文档里描述DragTarget* 等方法需要在DragSource*等方法之前被调用,于是我下断点调试,发现的确是DragTarget*等方法在DragSource*之后被调用了。

原因是我开始了cef的多线程消息循环multi_threaded_message_loop)。DragTarget*等方法在主程序的ui线程(因为用了多线程消息循环,所以主程序的ui线程和cef的ui线程是两个独立线程)里被调用了。他们内部发现线程并不是cef的ui线程,所以会被DragTarget*等方法的调用转到cef的ui线程。从而导致DragTarget*等方法的调用被延迟了,所以导致了最终的bug。

 

但是为什么DragTarget*等方法会在主程序的ui线程里触发呢?DragTarget*等方法是在StartDragging调用了win32的api ::DoDragDop而从同步触发的,StartDragging是在cef的ui线程被触发的,怎么同步触发到DragTarget*等方法就变成了主程序的ui线程了?

最终我发现是我之前说道的win32 api RegisterDragDrop的一个细节,我在主程序的ui线程里调用了这个api,如果在cef的ui线程里调用。那么DragTarget*等方法就会在cef的ui线程里被触发了。bug就解决了!

RegisterDragDrop内部会在调用这个API的线程里创建一个窗口,用过这个窗口来做消息循环模拟阻塞的过程,所以哪个线程调用RegisterDragDrop,就会在哪个线程阻塞并触发IDragTarget回调。见https://docs.microsoft.com/zh-cn/windows/win32/api/ole2/nf-ole2-registerdragdrop

总结:

执行::DoDragDrop时,会在调用RegisterDragDrop的线程触发的DragOver、DragLeave、Drop、Drop回调
进而调用browser_->GetHost()->DragTargetDragEnter、DragTargetDragOver、DragTargetDragLeave、DragTargetDrop
这几个cef接口内部发现不在cef ui线程触发,则会转发到cef ui线程
导致DragSourceEndedAt接口被调用时有部分DragTarget*方法没有被调用
最终拖拽效果就会有问题,详见DragSourceEndedAt接口描述
所以在cef ui线程调用RegisterDragDrop,让后面一系列操作都在cef ui线程里同步执行,则没问题
    
RegisterDragDrop内部会在调用这个API的线程里创建一个窗口,用过这个窗口来做消息循环模拟阻塞的过程
所以哪个线程调用RegisterDragDrop,就会在哪个线程阻塞并触发IDragTarget回调
见https://docs.microsoft.com/zh-cn/windows/win32/api/ole2/nf-ole2-registerdragdrop

题外话:

 

对于普通需求来说这样已经足够了,每一个browser对象都分配了一个对应的CefClient,都有对应的拖拽的实现。不过cef demo里面的实现是拖拽功能必须限制一个窗口内部只有一个browser,而我的需求是一个窗口内多个osr browser,每个browser都可以执行拖拽操作。为此我另外重写了cef demo附带的DropTargetWin,可以让一个窗口支持同时嵌入多个osr browser并完成拖拽。这个不是这篇分享的重点,我就不另外写了。

 

Redrain

QQ:491646717

2020.7.3

以上是关于cef osr拖拽功能实现的主要内容,如果未能解决你的问题,请参考以下文章

Duilib嵌入CEF禁止浏览器响应拖拽事件

移动端拖拽(模块化开发,触摸事件,webpack)

OSR推出在线SSL/TLS安全测试工具

动态加载 CEF, dynamic load cef

Cef功能开发经验总结

04:JS 拖拽功能的实现