2.0版本的富文本RICHEDIT20A的一些总结

        最近在修改即时通讯模块相关问题的时候,发现1.0版本的富文本控件CRichEditCtrl有一些bug和问题,比如选中文字时背景色是黑色;当输入白色文字时,选中后不显示文字等。所以决定使用2.0版本的富文本控件,但是远没有刚开始想的那么简单,遇到很多有疑惑的问题,下面就简单的总结一下。

        1、使用2.0版本的富文本控件的准备工作

        如何在程序中使用2.0版本的富文本控件呢?方法如下:

        方法一:(msdn上的做法,适用于用VC.NET及以后版本创建的工程)
            To update rich edit controls in existing Visual C++ applications to version 2.0,
            open the .RC file as text, change the class name of each rich edit control from   "RICHEDIT" to "RichEdit20a". 
            Then replace the call to AfxInitRichEdit with AfxInitRichEdit2.
       方法二:(以VC6对话框为例)--(我的是VC6的工程,所以使用这种方法)
       (1) 在CxxxApp中增加一成员变量 HMODULE  m_hMod;
       (2) 在CxxxApp::InitInstance()中添加一句m_hMod = LoadLibrary(_T("riched20.dll")),加载2.0版本的dll库;
             在CxxxApp::ExitInstance()中添加一句FreeLibrary(m_hMod),即在程序退出时,释放dll库;
       (3) 以已有的富文本控件的ID,搜索到.rc文件中的相关内容,将richedit控件的类名由"RICHEDIT" 修改成 "RICHEDIT20A",如下所示:

IDD_INFO_STATUS_DLG DIALOG DISCARDABLE  0, 0, 243, 219
STYLE WS_POPUP | WS_CLIPSIBLINGS
FONT 9, "宋体"
BEGIN
    PUSHBUTTON      "",IDC_CLOSE,221,7,15,14
    CONTROL         "",IDC_EDIT_INFO,"RICHEDIT20A",ES_MULTILINE | 
                    ES_AUTOVSCROLL | ES_READONLY | WS_VSCROLL | WS_TABSTOP,
                    25,37,195,148
END

        注意:在将"RICHEDIT" 替换成 "RICHEDIT20A"时,一定要所有的地方都替换掉,否则运行时在显示相关窗口时会出现ASSERT(::IsWindow(m_hWnd))错误。原因是程序载入的2.0版本的富文本控件,如果继续使用1.0版本,将会导致所在的窗口创建失败,从而在显示窗口的时候出现assert错误,比如程序会在下面的代码错报错(CInfoDlg对话框中使用到richedit,资源中忘记将"RICHEDIT" 替换成 "RICHEDIT20A"):

	CInfoDlg::Instance().Create( CInfoDlg::IDD, this->GetParent() );
	CInfoDlg::Instance().CenterWindow( this->GetParent() );
	CInfoDlg::Instance().ShowWindow( SW_HIDE ); // 程序会在此句报错

        2、在现有内容尾部追加新字符

        要在现有内容结尾处追加新字符,则要先调用SetSel选中最末尾处,然后调用ReplaceSel将新内容加上去。richedit 1.0是按多字节进行处理的,要在richedit 1.0控件中添加新字符,可以用以下的方法:

         nTotalTextLength = m_ChatDisplay.GetWindowTextLength(); // 调用CWnd的GetWindowTextLength接口获取当前字符数   
         m_ChatDisplay.SetSel( nTotalTextLength, nTotalTextLength );
         m_ChatDisplay.ReplaceSel( _T("打开文件") );

        richedit 2.0则包括RICHEDIT20A和RICHEDIT20W版本,前者是ANSI版本的,后者是宽字节版本。根据工程是否是UNICODE来选择使用哪个版本,本文主要讨论RICHEDIT20A版本的问题(因为我们的工程是非UNICODE的)。对于RICHEDIT20A,要添加新内容就不能使用上面的代码了,GetWindowTextLength()获得的是字节数,而RICHEDIT20A在计算字符位置时,是将一个汉字看成一个字符的,所以要使用上面的代码就不对了,应该使用下面的代码:

         m_ChatDisplay.SetSel( -1, -1 );  // 选中结尾处
         m_ChatDisplay.ReplaceSel( _T("打开文件") );

         RICHEDIT20A在计算字符位置时,将一个汉字看成一个字符,这个规则很重要,在很多地方需要注意。在下面添加超链接时会用到;在发送截图时,截图是以文件传输的方式发到对端的,对于接收端在收到聊天消息时,先用一个预设图片显示到richedit中,带截图文件接收完成后再找到对应的预设图片位置用实际图片将预设图片替换掉,这里面也牵涉到位置计算问题。              

        3、在RICHEDIT20A中添加自定义超链接               

        之前这个问题折腾了很长时间,一度以为RICHEDIT20A不能添加自定义超链接。因为不知道RICHEDIT20A在计算字符位置时,将一个汉字看成一个字符这个原则,一直使用CWnd的GetWindowTextLength接口来计算字符位置,导致添加链接失败。

        一般我们要改写MFC提供的控件,都是将MFC控件作为基类派生出来一个类,稍加改造实现我们想要的功能,所以我们可以从CRichEditCtrl派生一个类出来。要使richedit能响应超链接点击事件,需设置事件位ENM_LINK;要使richedit能自动识别超链接,比如我们输入网址,它会自动检测到并将值设置为超链接,需向richedit发送一个EM_AUTOURLDETECT消息,我们可以重写CRichEditCtrl的PreSubclassWindow接口,在这个接口中完成上述设置。另外,要能响应超链接点击事件,需要添加映射和效应函数,相关代码如下所示:

class CRichEditCtrlEx : public CRichEditCtrl
......
void CRichEditCtrlEx::PreSubclassWindow() 
{
        CRichEditCtrl::PreSubclassWindow();

	SetEventMask( ENM_LINK ); // 使richedit能响应超链接点击事件
	SendMessage( EM_AUTOURLDETECT, (WPARAM)true, 0 ); // 使richedit能自动检测超链接
}
ON_NOTIFY_REFLECT( EN_LINK, OnURLClick ) // 消息映射
void CRichEditCtrlEx::OnURLClick( NMHDR *pNmhdr, LRESULT *pResult ) // 链接点击响应函数
{
	ENLINK *pLink = (ENLINK*)pNmhdr;
	ASSERT(pLink);

	if ( pLink->msg == WM_LBUTTONUP )
	{
		SetSel(pLink->chrg);

		TCHAR cUrl[MAX_PATH * 2] = {0};
		GetSelText(cUrl);
		
		TCHAR achFilePath[MAX_PATH] = { 0 };
		CString strFilePath;
		CString strFilePathParam;

		// 对于文件传输,传输结束后在界面中会显示“打开文件”和“打开所在文件夹”的链接
 		if ( _tcscmp( cUrl, _T("打开文件") ) == 0 ) //“打开文件”链接
		{
			strFilePath = GetFilepath( pLink->chrg.cpMin, TRUE ); // 获取对应的路径信息
			strFilePathParam.Format( _T("\"%s\""), strFilePath ); // 加上双引号以防路径中有空格导致ShellExecute参数解析错误,2012/05/18
			ShellExecute(NULL, "open", strFilePathParam, NULL, NULL, SW_SHOWNORMAL);
 		}
		else if ( _tcscmp( cUrl, _T("打开所在文件夹") ) == 0 ) // “打开所在文件夹”链接
		{
			strFilePath = GetFilepath( pLink->chrg.cpMin, FALSE ); // 获取对应的路径信息
			CString strTemp; // 打开文件夹并选中文件,要使用“/select, ”选项
			strTemp.Format( _T("\"%s\""), strFilePath ); // 加上双引号以防路径中有空格导致ShellExecute参数解析错误,2012/05/18
			strFilePathParam = _T("/select, ");
            strFilePathParam += strTemp;
			ShellExecute( NULL, "open", _T("explorer.exe"), strFilePathParam, NULL, SW_SHOWNORMAL ); // 使用资源管理器打开
		}
		else
		{
			ShellExecute(NULL, "open", cUrl, NULL, NULL, SW_SHOWNORMAL);
		}
	}
	pResult = FALSE;
}   

        上面简单叙述了一下使用超链接的前期准备工作,下面着重说一下如何添加自定义超链接的事情。

        其实设置自定义超链接也容易,关键是找到超链接字符对象的位置,然后选中超链接字符,设置超链接格式即可。那么不能使用CWnd的GetWindowTextLength接口,如何得到超链接字符的位置呢?用下面的代码段即可实现:

	long nStart = 0;
	long nEnd = 0;
        m_ChatDisplay.SetSel( -1, -1 ); // 选中现有文字的末尾
	m_ChatDisplay.GetSel( nStart, nEnd ); // 得到末尾处的位置
        具体一点,比如在文件传输模块中,当文件接收完成后,将文件接收完成的信息写入到richedit中显示,此时需要紧接在后面添加“打开文件”和“打开所在文件夹”超链接,给用户提供快捷的查看接收到的文件的简洁途径,方便用户操作。下面以添加“打开文件”超链接为例:
		// 前面已经将文件传输提示信息加入到richedit中,下面紧接着在后面添加“打开文件”的超链接
                CHARFORMAT cf;
		ZeroMemory( &cf, sizeof(CHARFORMAT) );
		cf.cbSize = sizeof(CHARFORMAT);
		cf.dwMask = CFM_COLOR | CFM_FACE | CFM_LINK | CFM_SIZE /*| CFM_UNDERLINE*/;
		cf.dwEffects = CFE_LINK | ~CFE_AUTOCOLOR;
		cf.crTextColor = RGB( 0, 114, 193 ); // 文字颜色,这句改变颜色好像是无效的
		
		long nStart = 0;
		long nEnd = 0;
		long nStart2 = 0;
		long nEnd2 = 0;
		
		// 设置“打开文件”的超连接
		m_ChatDisplay.SetSel( -1, -1 ); // 选中现有文字的末尾
		m_ChatDisplay.GetSel( nStart, nEnd ); // 找到现有文字的末尾位置,并记录
		CString strLinkText = _T("打开文件");
		m_ChatDisplay.ReplaceSel( (LPCSTR)strLinkText );
		m_ChatDisplay.SetSel( -1, -1 ); // 选中现有文字的末尾
		m_ChatDisplay.GetSel( nStart2, nEnd2 ); // 找到添加“打开文件”后的末尾位置,并记录
		m_ChatDisplay.SetSel( nStart, nStart2 ); // 选中“打开文件”字样 
		m_ChatDisplay.SetSelectionCharFormat( cf ); // 设置超链接格式
        有人可能会说,这样处理有点麻烦,因为我们知道超链接的长度,按照RICHEDIT20A在计算字符位置时,将一个汉字看成一个字符这个原则,可以计算得到末尾位置,不用两次调用GetSel。事实上,超链接长度有时是未知的,是不好去实时计算的,特别是超链接文字中既包含字母又包含汉字的情况,所以还是有必要调用两次GetSel来获取末尾位置的。

        4、如何让自定义超链接执行指定的操作

        根据实际的需求,从超链接处找到相关的数据,在超链接点击的响应函数中来执行指定的操作。以文件传输为例,文件传输的界面如下图所示:

        首先,我要解析出文件路径,然后传递给ShellExecute函数去执行我们预定的操作。难点在于如何解析出文件路径,我这个地方采用的是笨办法,从“打开文件”超链接处向前找,因为提示文字使用统一的格式,考虑到路径长度一般不超过MAX_PATH长度,所以向前推MAX_PATH个位置,但是此处又要注意这一原则:RICHEDIT20A在计算字符位置时,将一个汉字看成一个字符,所以考虑到截取的字符传中可能包含汉字字符,所以在原有的长度基础上增加一倍,以免调用GetSelText时buf溢出。获取到字符串以后,根据提示文字的格式,就可以解析出具体的路径信息。由于上面已给出响应超链接单击的接口,下面接着给出解析路径的相关代码:

// 获取“打开文件”链接前面的文字,解析出文件的完整路径
CString CRichEditCtrlEx::GetFilepath( UINT nCurPos, BOOL bForOpenFile )
{
	UINT nTempCurPos = nCurPos;

	// 下面对文件路径的解析,严格按照提示信息的格式进行解析,提示信息的格式
	// 为:您成功接收了文件“E:\test-13.txt”。打开文件  打开所在文件夹 

	// 如果是为“打开所在文件夹”链接解析文件路径,要将链接前面的“打开文件  ”文字给
	// 偏移掉(“打开文件”四个汉字以及两个英文输入法下的空格)
    if ( !bForOpenFile ) 
    {
		nTempCurPos -= 2*1; // 偏移两个英文输入法下的空格
		nTempCurPos -= 4*1; // 4*1,偏移“打开文件”四个汉字
    }

	// 考虑文件路径最长不超过MAX_PATH长度,所以由“打开文件”链接的位置,找到前MAX_PATH长度的文字
	int nStartPos = 0;
	int nEndPos = 0;
	if ( nTempCurPos >= MAX_PATH*sizeof(TCHAR)+2*1 ) // 2*1表示将“打开文件夹”链接前的"”。"两个汉字字符去掉
	{
		nStartPos = nTempCurPos - MAX_PATH*sizeof(TCHAR) - 2*1;  // 2*1,保证nStartPos大于等于0
	}
	else
	{
		nStartPos = 0;
	}

	nEndPos = nTempCurPos-2*1; // 去掉链接前面的"”。"两个汉字字符

	// 对于RICHEDIT2.0有点奇怪,它在计算字符的位置时,将汉字看作一个字符,所以下面在计算nBufLen时要乘以2,
	// 防止buf越界
	int nBufLen = (nEndPos - nStartPos + 1)*2; // 防止选中文字长度大于buf长度,导致数组越界 

 	SetSel( nStartPos, nEndPos ); 
	TCHAR* pchText = new TCHAR[nBufLen];
	memset( pchText, 0, nBufLen );
	GetSelText( pchText );
    
	// 截取的文字中可能包含多个路径信息,下面要找到最后一个路径信息,即找到最后一个_T("“")
	CString strFilePath = pchText;
	int nPos = 0;
	int nTemp = strFilePath.Find( _T("“") );
	CString strTemp = strFilePath;
	while( nTemp != -1)
	{
		nPos += nTemp;
		strTemp = strTemp.Right(  strTemp.GetLength() - nTemp - 2 ); // 2表示将_T("“")字符偏移掉
		nTemp = strTemp.Find( _T("“") );
		if ( nTemp != -1 )
		{
			nPos += 2; // 2表示将_T("“")字符偏移掉
		}
	}

	int nLen = strFilePath.GetLength() - nPos - 2;
 	strFilePath = strFilePath.Right( strFilePath.GetLength() - nPos - 2 ); // 2表示将_T("“")字符偏移掉

	nLen = strFilePath.GetLength();

	delete []pchText;

	SetSel( -1, -1 );

	return strFilePath;
}

        上面的代码始终要结合RICHEDIT20A在计算字符位置时,将一个汉字看成一个字符这一原则来看,看起来可能有很多感觉别扭的地方,但为了实现我们的目标,不得不改造出上面的代码。

        以上就是将richedit1.0改造成2.0版本的RICHEDIT20A过程中遇到的一些细节问题,鉴于网上关于RICHEDIT20A细节处理方面的问题介绍的很少,所以在此分享出来。


你可能感兴趣的:(null,mfc,dll,输入法,Path)