windows聊天客户端:使用嵌入IE内核的方法

很多时候,如果客户端需要做个聊天消息的显示框,自然而然的使用richedit,但是随着聊天消息数的增加,内容形式的增加,显示和动态插入等等功能的增长,会发现richedit已经不能满足要求的。

    本文决定借用强大的IE内核,来finish。本文要设计到一些html,css,js部分内容,不懂的自学下,http://www.w3school.com.cn/。

   本文实现的功能,批量插入删除编辑多条图文信息,页面与主客户端的交互,。

考虑到效率问题​

​   首先是控件替换,MFC中用CHtmlView替换richedit,编辑自己head和js代码,示例如下:

​CString CChatMgr::GetChatJsHead()

{

//禁止右键弹出菜单和F5刷新,setWidthHeight()控制所有图片resize,

//setImgWidthHeight执行图片resize,gif图片除外

CString csTxFontSize;

csTxFontSize.Format("font-size:%dpx;",CHAT_TEXTFONT_SIZE);

CString csHead = "

\

 

 \

 \

 

 oncontextmenu=\"return false;\" onresize=\"setWidthHeight();\"\

 onkeydown=\"{if(event.keyCode==116){\

 event.keyCode=0;event.returnValue=false;}}\">\

 

";

return csHead;

}

注:在htmlview控件进行size变化的时候,显示的图片要随着窗口变大变小,所以本初的JS代码使用二分查找写了一个递归函数searchShowingImg进行查找图片,然后进行宽高修改。

编辑好了之后,写入htmlview,使用MFC的时候,都是通过LPDISPATCH​接口,根据IID获取相应的页面模块,都是固定的,我们直接调用就好了,示例如下。

void CSWHTMLCtrl::InsertContentInit(CString csJsHead,CString csTitle)

{

m_csHeadTitle = csTitle;

LPDISPATCH lpDispatch = NULL;

lpDispatch = this->GetHtmlDocument();

HRESULT hr;

IHTMLDocument2Ptr pHTMLDoc2 = NULL;

if (lpDispatch != NULL)

{

hr = lpDispatch->QueryInterface(IID_IHTMLDocument2, (LPVOID*)&pHTMLDoc2);

lpDispatch->Release();

}else

{

return;

}

m_csJsHead = csJsHead;

SAFEARRAY *pSafeArray = NULL;

pSafeArray = ::SafeArrayCreateVector(VT_VARIANT, 0, 1);

VARIANT *pChatElement = NULL;

if (pSafeArray)

{

hr = ::SafeArrayAccessData(pSafeArray,(LPVOID*) &pChatElement);

if (!SUCCEEDED(hr))

{

if (pHTMLDoc2)

{

pHTMLDoc2->close();

pHTMLDoc2.Release();

}

return;

}

else

{

pChatElement->vt= VT_BSTR;

pChatElement->bstrVal= m_csJsHead.AllocSysString();

hr = SafeArrayUnaccessData(pSafeArray);

if (SUCCEEDED(hr))

{

pHTMLDoc2->clear();

pHTMLDoc2->close();

hr = pHTMLDoc2->write(pSafeArray);

}

}

if (pSafeArray) 

{

SafeArrayDestroy(pSafeArray);

pSafeArray = NULL;

}

}

if (pHTMLDoc2)

{

pHTMLDoc2->close();

pHTMLDoc2.Release();

}

//初始化成功,安装响应点击链接的handle,用于页面内点击回调主程序

 InstallEventHandler();

m_bInitCont = TRUE;

}

void CSWHTMLCtrl::InstallEventHandler()

{

VP_TRACE_INFO(APP_MODID"> CSWHTMLCtrl::InstallEventHandler()");

if(m_dwEventCookie)   // 已安装,卸载先。最后一次安装的才有效

{

UninstallEventHandler();

}

IHTMLDocument2Ptr pHTMLDoc2 = NULL;

pHTMLDoc2 = this->GetHtmlDocument();

if (pHTMLDoc2 != NULL)

{

IConnectionPointContainerPtr pCPC = pHTMLDoc2;

IConnectionPointPtr pCP;

// 找到安装点

HRESULT hr = pCPC->FindConnectionPoint(DIID_HTMLDocumentEvents2, &pCP);

if (SUCCEEDED(hr))

{

IUnknown* pUnk = GetInterface(&IID_IUnknown);

//安装

hr = pCP->Advise(pUnk, &m_dwEventCookie);

if (SUCCEEDED(hr))

{

VP_TRACE_INFO(APP_MODID"- CSWHTMLCtrl::InstallEventHandler() success.");

}

pCP.Release();

}

pHTMLDoc2.Release();

}

}

注:类似接口IHTMLDocument2Ptr​有多个,我大致归类一下(msdn上可查),

​IHTMLDocument2Ptr:处理和页面内容相关业务

IHTMLDocument3Ptr​:处理dom节点相关业务,属性之类

IHTMLDocument3Ptr​:媒体文件,页面event

IHTMLDocument5Ptr:activate相关

使用之前要打开此接口功能,示例代码:​

#if defined(__IHTMLDocument2_INTERFACE_DEFINED__)

_COM_SMARTPTR_TYPEDEF(IHTMLDocument2, __uuidof(IHTMLDocument2));

#endif

接下来就是消息的插入了,我再上面的head编辑中,加入了一个parentnode节点,就是为了方便消息的前插和后插​,大概思路如下,插入的时候,编辑好消息内容,消息内容可以是单条,也可以是多条,以

content
作为节点,每天消息都编辑了一个id,写入该div的属性值当中,方便以后查找出来进行删除和编辑等操作。然后取出找到parentnode,由它控制前插或者后插入消息。

示例如下

HRESULT CSWHTMLCtrl::InsertContent(const CString& csContent,

  DWORD& dwID,

  BOOL bAfter,

  BOOL bCheckNeedScrollEnd)

{

if (!m_bInitCont)

{

InsertContentInit(m_csJsHead);

}

IHTMLDocument2Ptr pHTMLDoc2 = NULL;

IHTMLDocument3Ptr pHTMLDoc3 = NULL;

LPDISPATCH lpDispatch = NULL;

CComQIPtr pDivElement;

HRESULT hr;

lpDispatch = this->GetHtmlDocument();

if (lpDispatch != NULL)

{

hr = lpDispatch->QueryInterface(IID_IHTMLDocument2, (LPVOID*)&pHTMLDoc2);

if (!SUCCEEDED(hr))

{

lpDispatch->Release();

return S_FALSE;

}

hr = lpDispatch->QueryInterface(IID_IHTMLDocument3, (LPVOID*)&pHTMLDoc3);

lpDispatch->Release();

if (!SUCCEEDED(hr))

{

if (pHTMLDoc2)

{

pHTMLDoc2.Release();

}

return S_FALSE;

}

hr = pHTMLDoc2->createElement_x(CComBSTR("div"), &pDivElement);

if (SUCCEEDED(hr))

{

hr = pDivElement->put_innerHTML(_bstr_t((LPCSTR)csContent));

if (SUCCEEDED(hr))

{

CComQIPtr pNewNode;

CComQIPtr pParentBody ;

hr = pHTMLDoc3->getElementById(CComBSTR("parentnode"),&pParentBody);

if (SUCCEEDED(hr) && pParentBody)

{

CComQIPtr pParentNode = pParentBody;

  

CString csID;

csID.Format("%d",m_dwContentIndex);

pDivElement->put_id(_bstr_t(csID.GetBuffer()));

if (bAfter)

{

if (bCheckNeedScrollEnd)

{

m_bNeedScrollEnd = LastNodeShowInView(pHTMLDoc2,pParentNode);

}

//向最后插入节点

hr = pParentNode->appendChild(CComQIPtr(pDivElement), &pNewNode);

}else

{

CComQIPtr pFirstNode;

hr = pParentNode->get_firstChild(&pFirstNode);

if (SUCCEEDED(hr) && pFirstNode)

{

CComVariant varTheNode(pFirstNode);

hr = pParentNode->insertBefore(CComQIPtr(pDivElement),varTheNode, &pNewNode);

pFirstNode.Release();

}else

{

//第一个节点没找到,转为后插入的方式

hr = pParentNode->appendChild(CComQIPtr(pDivElement), &pNewNode);

}

}

if (SUCCEEDED(hr))

{

dwID = m_dwContentIndex;

m_dwContentIndex++;

}

 

 if (pNewNode)

{

pNewNode.Release();

}

pParentNode.Release();

pParentBody.Release();

}

}

pDivElement.Release();

}

 

 if (pHTMLDoc2)

 {

 pHTMLDoc2->close();

 pHTMLDoc2.Release();

 }

if (pHTMLDoc3)

{

pHTMLDoc3.Release();

}

}

return hr;

}

删除消息:​

HRESULT CSWHTMLCtrl::RemoveNodeByID(CString csID)

{

IHTMLDocument2Ptr pHTMLDoc2 = NULL;

IHTMLDocument3Ptr pHTMLDoc3 = NULL;

LPDISPATCH lpDispatch = NULL;

HRESULT hr;

lpDispatch = this->GetHtmlDocument();

if (lpDispatch != NULL)

{

hr = lpDispatch->QueryInterface(IID_IHTMLDocument2, (LPVOID*)&pHTMLDoc2);

if (!SUCCEEDED(hr))

{

lpDispatch->Release();

return S_FALSE;

}

hr = lpDispatch->QueryInterface(IID_IHTMLDocument3, (LPVOID*)&pHTMLDoc3);

lpDispatch->Release();

if (!SUCCEEDED(hr))

{

if (pHTMLDoc2)

{

pHTMLDoc2.Release();

}

return S_FALSE;

}

CComQIPtr pRemoveNodeBody ;

hr = pHTMLDoc3->getElementById(CComBSTR(csID.GetBuffer()),&pRemoveNodeBody);

if (SUCCEEDED(hr) && pRemoveNodeBody)

{

CComPtr pParentElment=NULL; 

hr = pRemoveNodeBody->get_parentElement(&pParentElment);

if (SUCCEEDED(hr) && pParentElment)

{

CComQIPtr pNewNode;

CComQIPtr pParentNode = pParentElment;

hr = pParentNode->removeChild(CComQIPtr(pRemoveNodeBody),&pNewNode);

if (pNewNode)

{

pNewNode.Release();

}

pParentNode.Release();

pParentElment.Release();

}

pRemoveNodeBody.Release();

}

if (pHTMLDoc2)

{

pHTMLDoc2->close();

pHTMLDoc2.Release();

}

if (pHTMLDoc3)

{

pHTMLDoc3.Release();

}

}

if (csID.CompareNoCase("title") == 0)

{

//删除title结点时,自动刷一下前插入的消息

DoDelayBeforeInsert();

m_bHasHeadTitle = FALSE;

}

return hr;

}

注:如果你要删除所有消息,直接把参数设置成parentnode节点就可以。


编辑节点:下面以更新某个节点的图片内容为例,找到该节点,通过setAttribute重新设置图片地址,修改其它内容的方法类似。

HRESULT CSWHTMLCtrl::UpdateImage(CString strID,CString strContent)

{

//strContent图片路径用网络路径

IHTMLDocument2Ptr pHTMLDoc2 = NULL;

IHTMLDocument3Ptr pHTMLDoc3 = NULL;

LPDISPATCH lpDispatch = NULL;

HRESULT hr;

do 

{

lpDispatch = this->GetHtmlDocument();

if (!lpDispatch)

{

break;

}

hr = lpDispatch->QueryInterface(IID_IHTMLDocument2, (LPVOID*)&pHTMLDoc2);

if (!SUCCEEDED(hr))

{

break;

}

hr = lpDispatch->QueryInterface(IID_IHTMLDocument3, (LPVOID*)&pHTMLDoc3);

if (!SUCCEEDED(hr))

{

break;

}

// CComQIPtr pParentBody ;

// hr = pHTMLDoc3->getElementById(CComBSTR("parentnode"),&pParentBody);

// if (SUCCEEDED(hr) && pParentBody)

// {

// CComQIPtr pParentNode = pParentBody;

// m_bNeedScrollEnd = LastNodeShowInView(pHTMLDoc2,pParentNode);

// pParentNode.Release();

// pParentBody.Release();

// }

//获取到该ID的图片集合,遍历,设置src属性

CComQIPtr pBeforeBodyCollection ;

hr = pHTMLDoc3->getElementsByName(CComBSTR(strID),&pBeforeBodyCollection);

if (!SUCCEEDED(hr))

{

break;

}

long iSize = 0;

hr = pBeforeBodyCollection->get_length(&iSize);

if (!SUCCEEDED(hr) || iSize==0)

{

pBeforeBodyCollection.Release();

break;

}

for (int i=0;i

{

_variant_t index;

index.vt=VT_I4;

index.intVal=i;

CComPtr spDispatch;

hr = pBeforeBodyCollection->item(index,index,&spDispatch);

if (!SUCCEEDED(hr) || spDispatch==NULL)

{

continue;

}

CComQIPtr pBody1;

hr = spDispatch->QueryInterface(IID_IHTMLElement, (LPVOID*)&pBody1);

if (!SUCCEEDED(hr) || pBody1==NULL)

{

spDispatch.Release();

continue;

}

CComVariant varTheNode(strContent.GetBuffer());

hr = pBody1->setAttribute(CComBSTR("src"),varTheNode);

CComQIPtr pElement = pBody1; 

int j = 0;//做个循环防错机制,查找了四层还没有找到div则略过

while(j<4 && pElement) // 逐层向上检查

{

j++;

_bstr_t strTagname;

pElement->get_tagName(&strTagname.GetBSTR());

if(_bstr_t("div") == strTagname || _bstr_t("DIV") == strTagname)

{

// 取得目标地址:

_variant_t vHref;

vHref.vt=VT_BSTR;

pElement->getAttribute(CComBSTR("id"),0,&vHref);

CStringA idName(vHref.bstrVal);

idName.Trim();

if (idName.GetLength() > 0)

{

CString csInnerHtml ;

BSTR pstrInner;

hr = pElement->get_innerHTML(&pstrInner);

if (SUCCEEDED(hr))

{

csInnerHtml = pstrInner;

SysFreeString(pstrInner);

int iID = atoi(idName);

if (m_AllInfoMap.size()>0 && m_AllInfoMap.find(iID)!=m_AllInfoMap.end())

{

CString csHtmlText;

csHtmlText.Format("

%s
",

idName, csInnerHtml);

m_AllInfoMap[iID] = csHtmlText;

}

}

}

pElement.Release();

break;

}

CComPtr pTempElment=NULL; 

pElement->get_parentElement(&pTempElment);

if (pTempElment)

{

pElement.Release();

pElement = pTempElment;

pTempElment.Release();

}else

{

pElement.Release();

break;

}

}

pBody1.Release();

spDispatch.Release();

}

if (pBeforeBodyCollection)

{

pBeforeBodyCollection.Release();

}

}while(0);

if (pHTMLDoc2)

{

pHTMLDoc2.Release();

pHTMLDoc2 = NULL;

}

if (pHTMLDoc3)

{

pHTMLDoc3.Release();

pHTMLDoc3 = NULL;

}

if (lpDispatch)

{

lpDispatch->Release();

lpDispatch = NULL;

}

return hr;

}

最后对页面内容的其它操作就不一一贴出来了,功能都是一通百通的,自己想想基本上都能实现,问题不大,下面是已经实现的功能。

windows聊天客户端:使用嵌入IE内核的方法

​​​接下来就是htmlview的回调主程序:

之前已经通过InstallEventHandler​设置了事件回调,然后在消息Map中加入响应函数OnJsCallBackFunc,在自己写的js代码中调用OnJsCallBackFunc即可实现回调,

/增加JS回调接口函数

BEGIN_DISPATCH_MAP(CSWHTMLCtrl, CHtmlView)

DISP_FUNCTION_ID(CSWHTMLCtrl,"HTMLELEMENTEVENTS2_ONCLICK",

DISPID_HTMLELEMENTEVENTS2_ONCLICK, OnClick,

VT_EMPTY, VTS_DISPATCH)

DISP_FUNCTION_ID(CSWHTMLCtrl,"HTMLELEMENTEVENTS2_ONDBLCLICK",

DISPID_HTMLELEMENTEVENTS2_ONDBLCLICK, OnDblClick,

VT_EMPTY, VTS_DISPATCH)

DISP_FUNCTION(CSWHTMLCtrl, "OnJsCallBackFunc", OnJsCallBackFunc, VT_BOOL, VTS_I4)

END_DISPATCH_MAP()