还是以windows上vs2017 release版本运行的程序为例。
接上一篇,我们只是将离屏渲染的每一帧图片QImage,以异步事件的方法发送到了目标QWiget上,然后再将QImage绘制到屏幕上。
留下了一些待处理的问题:
- 滚轮事件
- 鼠标的点击事件
- 鼠标hover到链接上,鼠标形状的变化
- 输入框的右键弹出菜单位置问题
- 键盘输入
- 按F5刷新网页
我们这篇博客就围绕这些问题进行处理。
一、离屏渲染的滚轮事件的处理
滚轮事件的处理最为简单。
在CefOSRWidget头文件中:
protected:
//其余代码与前面一篇相同,这里省略
void wheelEvent(QWheelEvent *event) override;
void CefOSRWidget::wheelEvent(QWheelEvent *event)
{
QPoint pos = event->pos();
CefRefPtr<CefBrowser> pBrower = getBrowser();
if (pBrower)
{
CefMouseEvent cefMouseEvent;
cefMouseEvent.x = pos.x();
cefMouseEvent.y = pos.y();
pBrower->GetHost()->SendMouseWheelEvent(cefMouseEvent, 0, event->delta());
}
//因为不必传递给下层,所以下面这行注释掉
//QWidget::wheelEvent(event);
}
cef_browser.h中的定义
class CefBrowserHost : public virtual CefBaseRefCounted {
public:
//其余省略
// Send a mouse wheel event to the browser. The |x| and |y| coordinates are
// relative to the upper-left corner of the view. The |deltaX| and |deltaY|
// values represent the movement delta in the X and Y directions respectively.
// In order to scroll inside select popups with window rendering disabled
// CefRenderHandler::GetScreenPoint should be implemented properly.
///
/*--cef()--*/
virtual void SendMouseWheelEvent(const CefMouseEvent& event,
int deltaX,
int deltaY) = 0;
//其余省略
};
就是向该Cef浏览器发送一个滚轮事件。
本篇博客中的其余做法,也是将手动发送事件给Cef浏览器。
效果:
垂直滚轮有了效果,但是发现鼠标点击滚动条两端的按钮没有效果,这时候,我们就需要手动发送鼠标事件给Cef浏览器了。
二、离屏渲染的鼠标事件的处理
class CefOSRWidget : public QWidget
{
Q_OBJECT
public:
//其余省略
protected:
void mousePressEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void mouseDoubleClickEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
//其余省略
private:
//将Qt的鼠标按钮转换成Cef中的鼠标按键(鼠标左键、中键,右键)
cef_mouse_button_type_t toCefMouseButton(Qt::MouseButton mouseButton);
cefosrwidget.cpp中相应的定义:
void CefOSRWidget::mousePressEvent(QMouseEvent *event)
{
QPoint pos = event->pos();
CefRefPtr<CefBrowser> pBrower = getBrowser();
if (pBrower)
{
CefMouseEvent cefMouseEvent;
cefMouseEvent.x = pos.x();
cefMouseEvent.y = pos.y();
cef_mouse_button_type_t cefMouseButton = toCefMouseButton(event->button());
pBrower->GetHost()->SendMouseClickEvent(cefMouseEvent, cefMouseButton, false, 1);
}
//QWidget::mousePressEvent(event);
}
void CefOSRWidget::mouseReleaseEvent(QMouseEvent *event)
{
QPoint pos = event->pos();
CefRefPtr<CefBrowser> pBrower = getBrowser();
if (pBrower)
{
CefMouseEvent cefMouseEvent;
cefMouseEvent.x = pos.x();
cefMouseEvent.y = pos.y();
cef_mouse_button_type_t cefMouseButton = toCefMouseButton(event->button());
pBrower->GetHost()->SendMouseClickEvent(cefMouseEvent, cefMouseButton, true, 1);
}
//QWidget::mouseReleaseEvent(event);
}
void CefOSRWidget::mouseDoubleClickEvent(QMouseEvent *event)
{
QPoint pos = event->pos();
CefRefPtr<CefBrowser> pBrower = getBrowser();
if (pBrower)
{
CefMouseEvent cefMouseEvent;
cefMouseEvent.x = pos.x();
cefMouseEvent.y = pos.y();
cef_mouse_button_type_t cefMouseButton = toCefMouseButton(event->button());
pBrower->GetHost()->SendMouseClickEvent(cefMouseEvent, cefMouseButton, false, 2);
}
//QWidget::mouseDoubleClickEvent(event);
}
void CefOSRWidget::mouseMoveEvent(QMouseEvent *event)
{
QPoint pos = event->pos();
CefRefPtr<CefBrowser> pBrower = getBrowser();
if (pBrower)
{
CefMouseEvent cefMouseEvent;
cefMouseEvent.x = pos.x();
cefMouseEvent.y = pos.y();
cefMouseEvent.modifiers = EVENTFLAG_LEFT_MOUSE_BUTTON;
pBrower->GetHost()->SendMouseMoveEvent(cefMouseEvent, false);
}
//QWidget::mouseMoveEvent(event);
}
cef_mouse_button_type_t CefOSRWidget::toCefMouseButton(Qt::MouseButton mouseButton)
{
cef_mouse_button_type_t cefMouseButton = MBT_LEFT;
switch (mouseButton)
{
case Qt::LeftButton:
cefMouseButton = MBT_LEFT;
break;
case Qt::RightButton:
cefMouseButton = MBT_RIGHT;
break;
case Qt::MiddleButton:
cefMouseButton = MBT_MIDDLE;
break;
default:
break;
}
return cefMouseButton;
}
小提示:
(1)cefosrwidget.ui文件中:
这里没有再嵌套QWidget,或者布局,就是为了避免坐标转换。刚开始的时候,我还是像前面几篇博客中一样,添加了一个contentWidget的QWiget在界面中,因此处理事件中的坐标的时候,每次都需要进行转换,最后发现什么都不要加,直接用这个QWidget来显示Cef,就不需要什么坐标转换了。
(2)关于Qt中事件的传递,我在做无边框窗口的移动的时候,如果在CefOSRWidget的鼠标事件中调用了QWidget::mouseXXEvent(event);则会将鼠标事件传递下去,这个在无边框可移动窗口中,会导致一些问题,正确的做法是在CefOSRWidget中的鼠标事件只自己处理,不再进行传递。事件的响应次序可以查看我之前的博客。
三、鼠标hover到链接上,鼠标形状的变化
平时,我们浏览网页的时候,鼠标hover到连接,或者一些按钮上会变成手掌的形状,而我们的离屏渲染窗口中缺没有这样鼠标形状变化。我个人的摸索,发现是CefOSRWidget要
setMouseTracking(true);
这样鼠标在没有按下的时候,也会一直检测鼠标的位置,一直响应mouseMoveEvent事件。
// Send a mouse move event to the browser. The |x| and |y| coordinates are
// relative to the upper-left corner of the view.
///
/*--cef()--*/
virtual void SendMouseMoveEvent(const CefMouseEvent& event,
bool mouseLeave) = 0;
二中已经在
void CefOSRWidget::mouseMoveEvent(QMouseEvent *event)
中添加了
pBrower->GetHost()->SendMouseMoveEvent(cefMouseEvent, false);
但是在我们的Cef窗口中,为什么鼠标形状还是没有变化呢?
于是我们回头去CefRenderHandler类中找:
class CefRenderHandler : public virtual CefBaseRefCounted {
public:
//中间省略
// Called when the browser's cursor has changed. If |type| is CT_CUSTOM then
// |custom_cursor_info| will be populated with the custom cursor information.
///
/*--cef()--*/
virtual void OnCursorChange(CefRefPtr<CefBrowser> browser,
CefCursorHandle cursor,
CursorType type,
const CefCursorInfo& custom_cursor_info) {}
};
原来是需要我们重写OnCursorChange
在simple_handler.h文件中:
class SimpleHandler : public CefClient,
public CefLifeSpanHandler,
public CefLoadHandler,
public CefFocusHandler,
public CefDisplayHandler,
public CefRenderHandler //新加的
{
public:
//其余省略
virtual void OnCursorChange(CefRefPtr<CefBrowser> browser,
CefCursorHandle cursor,
CursorType type,
const CefCursorInfo& custom_cursor_info) OVERRIDE;
};
在simple_handler.cpp中
void SimpleHandler::OnCursorChange(CefRefPtr<CefBrowser> browser, CefCursorHandle cursor, CefRenderHandler::CursorType type, const CefCursorInfo &custom_cursor_info)
{
ChangeCursorEvent *changeCursorEvent = new ChangeCursorEvent(type);
QCoreApplication::postEvent(m_receiver, changeCursorEvent);
// ::SetCursor(cursor);
}
直接调用::SetCursor(cursor);,不起作用,这个是windows API,改变鼠标形状的。
类似的,我们要自定义一个鼠标形状时间,将每次改变后的鼠标形状事件,发送到CefOSRWidget,在CefOSRWidget中进行改变。
class ChangeCursorEvent : public QEvent
{
public:
const static Type eventType = static_cast<Type>(QEvent::User+2);
explicit ChangeCursorEvent(cef_cursor_type_t shape_);
cef_cursor_type_t shape;
};
ChangeCursorEvent::ChangeCursorEvent(cef_cursor_type_t shape_) :QEvent(eventType)
{
shape = shape_;
}
在CefOSRWidget的event函数中:
bool CefOSRWidget::event(QEvent *event)
{
if (event->type() == UpdateEvent::eventType)
{
UpdateEvent *updateEvent = dynamic_cast<UpdateEvent*>(event);
m_image = updateEvent->image;
this->update();
return true;
}
else if (event->type() == ChangeCursorEvent::eventType)
{
ChangeCursorEvent *changeCursorEvent = dynamic_cast<ChangeCursorEvent*>(event);
cef_cursor_type_t cursorShape = changeCursorEvent->shape;
Qt::CursorShape qtCursorShape = Qt::ArrowCursor;
switch (cursorShape)
{
case CT_POINTER:
qtCursorShape = Qt::ArrowCursor;
break;
case CT_CROSS:
qtCursorShape = Qt::CrossCursor;
break;
case CT_HAND:
qtCursorShape = Qt::PointingHandCursor;
break;
case CT_IBEAM:
qtCursorShape = Qt::IBeamCursor;
break;
case CT_WAIT:
qtCursorShape = Qt::WaitCursor;
break;
default:
break;
}
this->setCursor(qtCursorShape);
return true;
}
return QWidget::event(event);
}
这里我们只处理了部分的鼠标形状,如果要做到完善的话,可以自定添加。
这时候,我们再运行,就看到了鼠标hover到链接上改变形状。
四、输入框的右键弹出菜单位置问题
我们在输入框右键的时候,弹出了右键菜单,可是这个右键菜单的问题有问题,我们来解决它。
同样的,在simple_handler.h类中去重写GetScreenPoint,这个也是CefRenderHandler类中虚函数。
// Called to retrieve the translation from view coordinates to actual screen
// coordinates. Return true if the screen coordinates were provided.
///
/*--cef()--*/
virtual bool GetScreenPoint(CefRefPtr<CefBrowser> browser,
int viewX,
int viewY,
int& screenX,
int& screenY) OVERRIDE;
simple_handler.cpp中
bool SimpleHandler::GetScreenPoint(CefRefPtr<CefBrowser> browser, int viewX, int viewY, int &screenX, int &screenY)
{
RECT clientRect;
if (!::GetWindowRect(m_hwnd, &clientRect))
return false;
screenX = clientRect.left + viewX;
screenY = clientRect.top + viewY;
return true;
}
运行效果:
这回右键菜单的位置大致差不多了。
五、键盘输入
在输入框中,发现键盘无法输入,我们同样需要在CefOSRWidget中添加:
cefosrwidget.h中添加
protected:
void keyPressEvent(QKeyEvent *event) override;
private:
CefKeyEvent toCefKeyEvent(QKeyEvent *event);
cefosrwidget.cpp文件中:
void CefOSRWidget::keyPressEvent(QKeyEvent *event)
{
CefRefPtr<CefBrowser> pBrower = getBrowser();
if (pBrower)
{
pBrower->GetHost()->SendKeyEvent(toCefKeyEvent(event));
}
QWidget::keyPressEvent(event);
}
CefKeyEvent CefOSRWidget::toCefKeyEvent(QKeyEvent *event)
{
CefKeyEvent cefKeyEvent;
cefKeyEvent.type = KEYEVENT_CHAR;
Qt::KeyboardModifiers keyboardModifiers = event->modifiers();
qDebug() << "keyboardModifiers:" << keyboardModifiers;
//只对Shift键和Ctrl键做处理,其余按需要进行处理的话(自己添加代码)
switch (keyboardModifiers)
{
case Qt::ShiftModifier:
cefKeyEvent.modifiers = EVENTFLAG_SHIFT_DOWN;
cefKeyEvent.type = KEYEVENT_CHAR; //如果是按下了shit键的话,则这里需要让Cef知道另外普通键的输入
break;
case Qt::ControlModifier:
cefKeyEvent.modifiers = EVENTFLAG_CONTROL_DOWN;
cefKeyEvent.type = KEYEVENT_KEYDOWN;
break;
// case Qt::AltModifier:
// cefKeyEvent.modifiers = EVENTFLAG_ALT_DOWN;
// cefKeyEvent.type = KEYEVENT_KEYDOWN;
// break;
default:
break;
}
int key = event->key();
qDebug() << "key: " << event->key();
qDebug() << "nativeScanCode: " << event->nativeScanCode();
qDebug() << "nativeVirtualKey: " << event->nativeVirtualKey();
cefKeyEvent.windows_key_code = event->key();
if (event->key() == Qt::Key_Backspace)
{
qDebug() << "event->key() == Qt::Key_Backspace";
cefKeyEvent.type = KEYEVENT_KEYDOWN;
cefKeyEvent.windows_key_code = static_cast<int>(event->nativeVirtualKey());
}
if (key >= Qt::Key_F1 && key <= Qt::Key_F12)
{
qDebug() << "Qt::Key_F1";
cefKeyEvent.type = KEYEVENT_KEYDOWN;
}
return cefKeyEvent;
}
运行结果:
可以输入,但是只能输入数字、符号和英文大写,无法输入中文。
本教程只是个抛转引入,因为Cef wiki文档上语焉不详,有些需要自己摸索。
个人猜测:需要设置Cef能接受的语言,比如中文,在键盘输入事件中,去区分大小写等。
六、按键盘上的F5键刷新网页
(1)自定义事件
class ReloadBrowerEvent : public QEvent
{
public:
const static Type eventType = static_cast<Type>(QEvent::User+3);
explicit ReloadBrowerEvent();
};
ReloadBrowerEvent::ReloadBrowerEvent() :QEvent(eventType)
{
}
(2)在cefosrwidget.cpp中
CefKeyEvent CefOSRWidget::toCefKeyEvent(QKeyEvent *event)
{
//其余代码与上面相同,这里省略
if (event->key() == Qt::Key_F5)
{
qDebug() << "Qt::Key_F5"; //刷新
ReloadBrowerEvent *reloadBrowerEvent = new ReloadBrowerEvent();
QCoreApplication::sendEvent(this, reloadBrowerEvent);
}
return cefKeyEvent;
}
bool CefOSRWidget::event(QEvent *event)
{
if (event->type() == UpdateEvent::eventType)
{
UpdateEvent *updateEvent = dynamic_cast<UpdateEvent*>(event);
m_image = updateEvent->image;
this->update();
return true;
}
else if (event->type() == ChangeCursorEvent::eventType)
{
ChangeCursorEvent *changeCursorEvent = dynamic_cast<ChangeCursorEvent*>(event);
cef_cursor_type_t cursorShape = changeCursorEvent->shape;
Qt::CursorShape qtCursorShape = Qt::ArrowCursor;
switch (cursorShape)
{
case CT_POINTER:
qtCursorShape = Qt::ArrowCursor;
break;
case CT_CROSS:
qtCursorShape = Qt::CrossCursor;
break;
case CT_HAND:
qtCursorShape = Qt::PointingHandCursor;
break;
case CT_IBEAM:
qtCursorShape = Qt::IBeamCursor;
break;
case CT_WAIT:
qtCursorShape = Qt::WaitCursor;
break;
default:
break;
}
this->setCursor(qtCursorShape);
return true;
}else if (event->type() == ReloadBrowerEvent::eventType)
{
CefRefPtr<CefBrowser> pBrower = getBrowser();
if (pBrower)
{
pBrower->Reload();
}
}
return QWidget::event(event);
}
按F5就能刷新网页了。
百度云盘
链接:https://pan.baidu.com/s/1NFJhg0xhRvmA1h4dpzyniA
提取码:2kp5
个人的一点体会:
在离屏渲染事件中,一般是CefRenderHandler中相应的接口,要么在QWidget上将事件传递给Cef,要么是在CefRenderHandler中相应的接口中获取变化后的数据,再用postEvent发送自定义事件,将该数据传递给QWidget,进行相应处理。
Qt相关知识:
(1)事件的传递顺序
(2)postEvent和sendEvent的区别
接下来需要注意的问题:
Cef关闭流程,如果关闭流程处理不好,则会出现关闭的时候崩溃的问题,Cef关闭流程将在接下来的教程进行介绍。