NO IMAGE

有時你可能想在C 程式碼中直接操作HTML中的某個元素,比如改變某個按鈕的狀態(文字、顏色)等,此時可以使用CEF提供的CefDomVisitor、CefDOMDocument、CefDomNode這三個類,包含cef_dom.h即可。

我們可以用它們完成下列任務:

  • 使用DOM模型訪問HTML的各種節點(Element、Attribute、Text、CDATA、Comment、Document等)
  • 修改某個元素的屬性
  • 修改某個Text節點的值

下面簡要說說各個類的用法。

CefDOMDocument

CefDOMDocument對應JS裡的document,不過功能少一些,類宣告如下:

class CefDOMDocument : public virtual CefBase {
public:
typedef cef_dom_document_type_t Type;
///
// Returns the document type.
///
/*--cef(default_retval=DOM_DOCUMENT_TYPE_UNKNOWN)--*/
virtual Type GetType() =0;
///
// Returns the root document node.
///
/*--cef()--*/
virtual CefRefPtr<CefDOMNode> GetDocument() =0;
///
// Returns the BODY node of an HTML document.
///
/*--cef()--*/
virtual CefRefPtr<CefDOMNode> GetBody() =0;
///
// Returns the HEAD node of an HTML document.
///
/*--cef()--*/
virtual CefRefPtr<CefDOMNode> GetHead() =0;
///
// Returns the title of an HTML document.
///
/*--cef()--*/
virtual CefString GetTitle() =0;
///
// Returns the document element with the specified ID value.
///
/*--cef()--*/
virtual CefRefPtr<CefDOMNode> GetElementById(const CefString& id) =0;
///
// Returns the node that currently has keyboard focus.
///
/*--cef()--*/
virtual CefRefPtr<CefDOMNode> GetFocusedNode() =0;
///
// Returns true if a portion of the document is selected.
///
/*--cef()--*/
virtual bool HasSelection() =0;
///
// Returns the selection offset within the start node.
///
/*--cef()--*/
virtual int GetSelectionStartOffset() =0;
///
// Returns the selection offset within the end node.
///
/*--cef()--*/
virtual int GetSelectionEndOffset() =0;
///
// Returns the contents of this selection as markup.
///
/*--cef()--*/
virtual CefString GetSelectionAsMarkup() =0;
///
// Returns the contents of this selection as text.
///
/*--cef()--*/
virtual CefString GetSelectionAsText() =0;
///
// Returns the base URL for the document.
///
/*--cef()--*/
virtual CefString GetBaseURL() =0;
///
// Returns a complete URL based on the document base URL and the specified
// partial URL.
///
/*--cef()--*/
virtual CefString GetCompleteURL(const CefString& partialURL) =0;
};

如你所見,它能獲取一些字串值(URL、標題等),能根據id查詢某個元素(在JS裡我們最常用的方式),能返回Document、Head、Body等節點,這些節點的型別是CefDOMNode。

注意這個類的方法只能在Renderer程序的主執行緒上呼叫(TID_RENDERER)。

CefDOMNode

在 HTML DOM (文件物件模型)中,每個部分都是節點:

  • 文件本身是文件節點
  • 所有 HTML 元素是元素節點
  • 所有 HTML 屬性是屬性節點
  • HTML 元素內的文字是文字節點
  • 註釋是註釋節點

CefDOMNode代表了一個HTML DOM節點,它的宣告如下:

class CefDOMNode : public virtual CefBase {
public:
typedef std::map<CefString, CefString> AttributeMap;
typedef cef_dom_node_type_t Type;
///
// Returns the type for this node.
///
/*--cef(default_retval=DOM_NODE_TYPE_UNSUPPORTED)--*/
virtual Type GetType() =0;
///
// Returns true if this is a text node.
///
/*--cef()--*/
virtual bool IsText() =0;
///
// Returns true if this is an element node.
///
/*--cef()--*/
virtual bool IsElement() =0;
///
// Returns true if this is an editable node.
///
/*--cef()--*/
virtual bool IsEditable() =0;
///
// Returns true if this is a form control element node.
///
/*--cef()--*/
virtual bool IsFormControlElement() =0;
///
// Returns the type of this form control element node.
///
/*--cef()--*/
virtual CefString GetFormControlElementType() =0;
///
// Returns true if this object is pointing to the same handle as |that|
// object.
///
/*--cef()--*/
virtual bool IsSame(CefRefPtr<CefDOMNode> that) =0;
///
// Returns the name of this node.
///
/*--cef()--*/
virtual CefString GetName() =0;
///
// Returns the value of this node.
///
/*--cef()--*/
virtual CefString GetValue() =0;
///
// Set the value of this node. Returns true on success.
///
/*--cef()--*/
virtual bool SetValue(const CefString& value) =0;
///
// Returns the contents of this node as markup.
///
/*--cef()--*/
virtual CefString GetAsMarkup() =0;
///
// Returns the document associated with this node.
///
/*--cef()--*/
virtual CefRefPtr<CefDOMDocument> GetDocument() =0;
///
// Returns the parent node.
///
/*--cef()--*/
virtual CefRefPtr<CefDOMNode> GetParent() =0;
///
// Returns the previous sibling node.
///
/*--cef()--*/
virtual CefRefPtr<CefDOMNode> GetPreviousSibling() =0;
///
// Returns the next sibling node.
///
/*--cef()--*/
virtual CefRefPtr<CefDOMNode> GetNextSibling() =0;
///
// Returns true if this node has child nodes.
///
/*--cef()--*/
virtual bool HasChildren() =0;
///
// Return the first child node.
///
/*--cef()--*/
virtual CefRefPtr<CefDOMNode> GetFirstChild() =0;
///
// Returns the last child node.
///
/*--cef()--*/
virtual CefRefPtr<CefDOMNode> GetLastChild() =0;
// The following methods are valid only for element nodes.
///
// Returns the tag name of this element.
///
/*--cef()--*/
virtual CefString GetElementTagName() =0;
///
// Returns true if this element has attributes.
///
/*--cef()--*/
virtual bool HasElementAttributes() =0;
///
// Returns true if this element has an attribute named |attrName|.
///
/*--cef()--*/
virtual bool HasElementAttribute(const CefString& attrName) =0;
///
// Returns the element attribute named |attrName|.
///
/*--cef()--*/
virtual CefString GetElementAttribute(const CefString& attrName) =0;
///
// Returns a map of all element attributes.
///
/*--cef()--*/
virtual void GetElementAttributes(AttributeMap& attrMap) =0;
///
// Set the value for the element attribute named |attrName|. Returns true on
// success.
///
/*--cef()--*/
virtual bool SetElementAttribute(const CefString& attrName,
const CefString& value) =0;
///
// Returns the inner text of the element.
///
/*--cef()--*/
virtual CefString GetElementInnerText() =0;
};

注意這個類的方法只能在Renderer程序的主執行緒上呼叫(TID_RENDERER)。

結合對HTML DOM節點的理解以及上面的程式碼,就能理解我們能使用CefDOMNode做什麼:

  • 使用IsXXX或GetType判斷節點型別
  • 使用GetNextSibling、GetPreviousSibling遍歷兄弟節點
  • 如果是Text節點(葉子節點),SetValue可以改變其文字
  • 如果是Element節點,可以使用GetFirstChild、GetLastChild獲取孩子,使用SetElementAttribute(s)改變屬性,使用GetElementAttibute(s)獲取屬性

HTML DOM中的Element,有appendChild、insertBefore等方法,可以很方便地動態插入節點改變DOM和網頁展示效果,而這個CefDOMNode就沒有相應的方法,好像不太方便……

CefDOMVisitor

這個類的宣告如下:

class CefDOMVisitor : public virtual CefBase {
public:
///
// Method executed for visiting the DOM. The document object passed to this
// method represents a snapshot of the DOM at the time this method is
// executed. DOM objects are only valid for the scope of this method. Do not
// keep references to or attempt to access any DOM objects outside the scope
// of this method.
///
/*--cef()--*/
virtual void Visit(CefRefPtr<CefDOMDocument> document) =0;
};

要訪問或修改HTML DOM,就必須實現這個類,然後將其物件傳遞給CefFrame::VisitDOM(CefRefPtr visitor)方法,最後你的Visit方法就被呼叫來訪問或修改HTML DOM。

看一個簡單的實現,顯示DomVisitTestor類的宣告:

class DomVisitTestor : public CefDOMVisitor
{
public:
DomVisitTestor();
void TestAccess(CefRefPtr<CefDOMDocument> document);
void TestModify(CefRefPtr<CefDOMDocument> document);
void Visit(CefRefPtr<CefDOMDocument> document) OVERRIDE;
IMPLEMENT_REFCOUNTING(DomVisitTestor);
};

然後是DomVisitTestor的實現:

void DomVisitTestor::TestAccess(CefRefPtr<CefDOMDocument> document)
{
OutputDebugStringW(L"DomVisitTestor::TestAccess\r\n");
OutputDebugStringW(document->GetTitle().ToWString().c_str());
OutputDebugStringW(document->GetBaseURL().ToWString().c_str());
CefRefPtr<CefDOMNode> headNode = document->GetHead();
OutputDebugStringW(headNode->GetName().ToWString().c_str());
OutputDebugStringW(headNode->GetAsMarkup().ToWString().c_str());
wchar_t szLog[512] = { 0 };
if (headNode->HasChildren())
{
CefRefPtr<CefDOMNode> childNode = headNode->GetFirstChild();
do 
{
swprintf_s(szLog, 256, L"node name -%s, type-%d, value-%s\r\n",
childNode->GetName().ToWString().c_str(), childNode->GetType(), childNode->GetValue());
OutputDebugStringW(szLog);
} while ( (childNode = childNode->GetNextSibling()).get() );
}
CefRefPtr<CefDOMNode> bodyNode = document->GetBody();
OutputDebugStringW(bodyNode->GetAsMarkup().ToWString().c_str());
if (bodyNode->HasChildren())
{
CefRefPtr<CefDOMNode> childNode = bodyNode->GetFirstChild();
do
{
swprintf_s(szLog, 256, L"node name -%s, type-%d, value-%s\r\n",
childNode->GetName().ToWString().c_str(), childNode->GetType(), childNode->GetValue());
OutputDebugStringW(szLog);
} while ((childNode = childNode->GetNextSibling()).get());
}
}
void DomVisitTestor::TestModify(CefRefPtr<CefDOMDocument> document)
{
OutputDebugStringW(L"DomVisitTestor::TestModify\r\n");
CefRefPtr<CefDOMNode> bodyNode = document->GetBody();
if (bodyNode->HasChildren())
{
CefRefPtr<CefDOMNode> childNode = bodyNode->GetFirstChild();
wchar_t szLog[512] = { 0 };
do{
swprintf_s(szLog, 256, L"node name -%s,tagName-%s type-%d, value-%s\r\n",
childNode->GetName().ToWString().c_str(), 
childNode->GetElementTagName().ToWString().c_str(),
childNode->GetType(), childNode->GetValue());
OutputDebugStringW(szLog);
if (childNode->IsElement() && childNode->GetElementTagName() == "H1"
&& childNode->GetElementAttribute("id") == "hello")
{
CefRefPtr<CefDOMNode> textNode = childNode->GetFirstChild();
swprintf_s(szLog, 512, L"found hello, text - %s\r\n", textNode->GetValue().ToWString().c_str());
OutputDebugStringW(szLog);
textNode->SetValue("Hello World Modified!");
break;
}
} while ((childNode = childNode->GetNextSibling()).get());
}
CefRefPtr<CefDOMNode> hello = document->GetElementById("hello");
if (hello.get())
{
hello->SetElementAttribute("align", "center");
OutputDebugStringW(L"Change hello align\r\n");
}
}
void DomVisitTestor::Visit(CefRefPtr<CefDOMDocument> document)
{
TestAccess(document);
TestModify(document);
}

注意,這個類的方法也應當在Renderer程序的主執行緒(TID_RENDERER)上使用。

測試用的HTML檔案如下:

<!DOCTYPE html>
<html>
<!--
Copyright (c) 2016 foruok(微信訂閱號“程式視界”)
-->
<style type="text/css">
#divtest
{
position: relative;
left: 30px;
top: 10px;
padding: 10px;
width: 300px;
height: 200px;
background-color: gray;
}
</style>
<head>
<script type="text/javascript">
function test(){
window.DomVisitTest();
}
</script>
<title>Dom Visit Test</title>
</head>
<body>
<h1 id="hello">Hello</h1>
<form>
<input  type="button" value="VisitDom" onclick="test()"/>
</form>
one<br>two
<p>This is text</p>
<div id="divtest">
<p>div hello</p>
</div>
</body>
</html>

你可能注意到我給VisitDom按鈕關聯的test()方法內呼叫了window.DomVisitTest()方法,該方法是我在C 程式碼中繫結到window物件上的,參考CEF中JavaScript與C 互動


就這樣吧。

其他參考文章詳見我的專欄:【CEF與PPAPI開發】。