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节点,就是为了方便消息的前插和后插,大概思路如下,插入的时候,编辑好消息内容,消息内容可以是单条,也可以是多条,以
示例如下
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("
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;
}
最后对页面内容的其它操作就不一一贴出来了,功能都是一通百通的,自己想想基本上都能实现,问题不大,下面是已经实现的功能。
接下来就是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()