1. 程式人生 > >2.0版本的富文字RICHEDIT20A的一些總結

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",如下所示:

  1. IDD_INFO_STATUS_DLG DIALOG DISCARDABLE  0, 0, 243, 219  
  2. STYLE WS_POPUP | WS_CLIPSIBLINGS  
  3. FONT 9, "宋體"
  4. BEGIN  
  5.     PUSHBUTTON      "",IDC_CLOSE,221,7,15,14  
  6.     CONTROL         "",IDC_EDIT_INFO,"RICHEDIT20A",ES_MULTILINE |   
  7.                     ES_AUTOVSCROLL | ES_READONLY | WS_VSCROLL | WS_TABSTOP,  
  8.                     25,37,195,148  
  9. END  

        注意:在將"RICHEDIT" 替換成 "RICHEDIT20A"時,一定要所有的地方都替換掉,否則執行時在顯示相關視窗時會出現ASSERT(::IsWindow(m_hWnd))錯誤。原因是程式載入的2.0版本的富文字控制元件,如果繼續使用1.0版本,將會導致所在的視窗建立失敗,從而在顯示視窗的時候出現assert錯誤,比如程式會在下面的程式碼錯報錯(CInfoDlg對話方塊中使用到richedit,資源中忘記將"RICHEDIT" 替換成 "RICHEDIT20A"):

  1. CInfoDlg::Instance().Create( CInfoDlg::IDD, 
    this->GetParent() );  
  2. CInfoDlg::Instance().CenterWindow( this->GetParent() );  
  3. CInfoDlg::Instance().ShowWindow( SW_HIDE ); // 程式會在此句報錯

      2、在現有內容尾部追加新字元

        要在現有內容結尾處追加新字元,則要先呼叫SetSel選中最末尾處,然後呼叫ReplaceSel將新內容加上去。richedit 1.0是按多位元組進行處理的,要在richedit 1.0控制元件中新增新字元,可以用以下的方法:

  1. nTotalTextLength = m_ChatDisplay.GetWindowTextLength(); // 呼叫CWnd的GetWindowTextLength介面獲取當前字元數   
  2. m_ChatDisplay.SetSel( nTotalTextLength, nTotalTextLength );  
  3. m_ChatDisplay.ReplaceSel( _T("開啟檔案") );  

        richedit 2.0則包括RICHEDIT20A和RICHEDIT20W版本,前者是ANSI版本的,後者是寬位元組版本。根據工程是否是UNICODE來選擇使用哪個版本,本文主要討論RICHEDIT20A版本的問題(因為我們的工程是非UNICODE的)。對於RICHEDIT20A,要新增新內容就不能使用上面的程式碼了,GetWindowTextLength()獲得的是位元組數,而RICHEDIT20A在計算字元位置時,是將一個漢字看成一個字元的,所以要使用上面的程式碼就不對了,應該使用下面的程式碼:

  1. m_ChatDisplay.SetSel( -1, -1 );  // 選中結尾處
  2. m_ChatDisplay.ReplaceSel( _T("開啟檔案") );  

         RICHEDIT20A在計算字元位置時,將一個漢字看成一個字元,這個規則很重要,在很多地方需要注意。在下面新增超連結時會用到;在傳送截圖時,截圖是以檔案傳輸的方式發到對端的,對於接收端在收到聊天訊息時,先用一個預設圖片顯示到richedit中,帶截圖檔案接收完成後再找到對應的預設圖片位置用實際圖片將預設圖片替換掉,這裡面也牽涉到位置計算問題。              

        3、在RICHEDIT20A中新增自定義超連結             

        之前這個問題折騰了很長時間,一度以為RICHEDIT20A不能新增自定義超連結。因為不知道RICHEDIT20A在計算字元位置時,將一個漢字看成一個字元這個原則,一直使用CWnd的GetWindowTextLength介面來計算字元位置,導致新增連結失敗。

        一般我們要改寫MFC提供的控制元件,都是將MFC控制元件作為基類派生出來一個類,稍加改造實現我們想要的功能,所以我們可以從CRichEditCtrl派生一個類出來。要使richedit能響應超連結點選事件,需設定事件位ENM_LINK;要使richedit能自動識別超連結,比如我們輸入網址,它會自動檢測到並將值設定為超連結,需向richedit傳送一個EM_AUTOURLDETECT訊息,我們可以重寫CRichEditCtrl的PreSubclassWindow介面,在這個介面中完成上述設定。另外,要能響應超連結點選事件,需要新增對映和效應函式,相關程式碼如下所示:

  1. class CRichEditCtrlEx : public CRichEditCtrl  
  2. ......  
  1. void CRichEditCtrlEx::PreSubclassWindow()   
  2. {  
  3.         CRichEditCtrl::PreSubclassWindow();  
  4.     SetEventMask( ENM_LINK ); // 使richedit能響應超連結點選事件
  5.     SendMessage( EM_AUTOURLDETECT, (WPARAM)true, 0 ); // 使richedit能自動檢測超連結
  6. }  
  1. ON_NOTIFY_REFLECT( EN_LINK, OnURLClick ) // 訊息對映
  1. void CRichEditCtrlEx::OnURLClick( NMHDR *pNmhdr, LRESULT *pResult ) // 連結點選響應函式
  2. {  
  3.     ENLINK *pLink = (ENLINK*)pNmhdr;  
  4.     ASSERT(pLink);  
  5.     if ( pLink->msg == WM_LBUTTONUP )  
  6.     {  
  7.         SetSel(pLink->chrg);  
  8.         TCHAR cUrl[MAX_PATH * 2] = {0};  
  9.         GetSelText(cUrl);  
  10.         TCHAR achFilePath[MAX_PATH] = { 0 };  
  11.         CString strFilePath;  
  12.         CString strFilePathParam;  
  13.         // 對於檔案傳輸,傳輸結束後在介面中會顯示“開啟檔案”和“開啟所在資料夾”的連結
  14.         if ( _tcscmp( cUrl, _T("開啟檔案") ) == 0 ) //“開啟檔案”連結
  15.         {  
  16.             strFilePath = GetFilepath( pLink->chrg.cpMin, TRUE ); // 獲取對應的路徑資訊
  17.             strFilePathParam.Format( _T("\"%s\""), strFilePath ); // 加上雙引號以防路徑中有空格導致ShellExecute引數解析錯誤,2012/05/18
  18.             ShellExecute(NULL, "open", strFilePathParam, NULL, NULL, SW_SHOWNORMAL);  
  19.         }  
  20.         elseif ( _tcscmp( cUrl, _T("開啟所在資料夾") ) == 0 ) // “開啟所在資料夾”連結
  21.         {  
  22.             strFilePath = GetFilepath( pLink->chrg.cpMin, FALSE ); // 獲取對應的路徑資訊
  23.             CString strTemp; // 開啟資料夾並選中檔案,要使用“/select, ”選項
  24.             strTemp.Format( _T("\"%s\""), strFilePath ); // 加上雙引號以防路徑中有空格導致ShellExecute引數解析錯誤,2012/05/18
  25.             strFilePathParam = _T("/select, ");  
  26.             strFilePathParam += strTemp;  
  27.             ShellExecute( NULL, "open", _T("explorer.exe"), strFilePathParam, NULL, SW_SHOWNORMAL ); // 使用資源管理器開啟
  28.         }  
  29.         else
  30.         {  
  31.             ShellExecute(NULL, "open", cUrl, NULL, NULL, SW_SHOWNORMAL);  
  32.         }  
  33.     }  
  34.     pResult = FALSE;  
  35. }     

        上面簡單敘述了一下使用超連結的前期準備工作,下面著重說一下如何新增自定義超連結的事情。

        其實設定自定義超鏈接也容易,關鍵是找到超連結字元物件的位置,然後選中超連結字元,設定超連結格式即可。那麼不能使用CWnd的GetWindowTextLength介面,如何得到超連結字元的位置呢?用下面的程式碼段即可實現:

  1. long nStart = 0;  
  2. long nEnd = 0;  
  3.        m_ChatDisplay.SetSel( -1, -1 ); // 選中現有文字的末尾
  4. m_ChatDisplay.GetSel( nStart, nEnd ); // 得到末尾處的位置
        具體一點,比如在檔案傳輸模組中,當檔案接收完成後,將檔案接收完成的資訊寫入到richedit中顯示,此時需要緊接在後面新增“開啟檔案”和“開啟所在資料夾”超連結,給使用者提供快捷的檢視接收到的檔案的簡潔途徑,方便使用者操作。下面以新增“開啟檔案”超連結為例:
  1. // 前面已經將檔案傳輸提示資訊加入到richedit中,下面緊接著在後面新增“開啟檔案”的超連結
  2.               CHARFORMAT cf;  
  3. ZeroMemory( &cf, sizeof(CHARFORMAT) );  
  4. cf.cbSize = sizeof(CHARFORMAT);  
  5. cf.dwMask = CFM_COLOR | CFM_FACE | CFM_LINK | CFM_SIZE /*| CFM_UNDERLINE*/;  
  6. cf.dwEffects = CFE_LINK | ~CFE_AUTOCOLOR;  
  7. cf.crTextColor = RGB( 0, 114, 193 ); <span style="font-weight: bold;">// 文字顏色,這句改變顏色好像是無效的</span>
  8. long nStart = 0;  
  9. long nEnd = 0;  
  10. long nStart2 = 0;  
  11. long nEnd2 = 0;  
  12. // 設定“開啟檔案”的超連線
  13. m_ChatDisplay.SetSel( -1, -1 ); // 選中現有文字的末尾
  14. m_ChatDisplay.GetSel( nStart, nEnd ); // 找到現有文字的末尾位置,並記錄
  15. CString strLinkText = _T("開啟檔案");  
  16. m_ChatDisplay.ReplaceSel( (LPCSTR)strLinkText );  
  17. m_ChatDisplay.SetSel( -1, -1 ); // 選中現有文字的末尾
  18. m_ChatDisplay.GetSel( nStart2, nEnd2 ); // 找到新增“開啟檔案”後的末尾位置,並記錄
  19. m_ChatDisplay.SetSel( nStart, nStart2 ); // 選中“開啟檔案”字樣 
  20. m_ChatDisplay.SetSelectionCharFormat( cf ); // 設定超連結格式
        有人可能會說,這樣處理有點麻煩,因為我們知道超連結的長度,按照RICHEDIT20A在計算字元位置時,將一個漢字看成一個字元這個原則,可以計算得到末尾位置,不用兩次呼叫GetSel。事實上,超連結長度有時是未知的,是不好去實時計算的,特別是超連結文字中既包含字母又包含漢字的情況,所以還是有必要呼叫兩次GetSel來獲取末尾位置的。

        4、如何讓自定義超連結執行指定的操作

根據實際的需求,從超連結處找到相關的資料,在超連結點選的響應函式中來執行指定的操作。以檔案傳輸為例,檔案傳輸的介面如下圖所示:

        首先,我要解析出檔案路徑,然後傳遞給ShellExecute函式去執行我們預定的操作。難點在於如何解析出檔案路徑,我這個地方採用的是笨辦法,從“開啟檔案”超連結處向前找,因為提示文字使用統一的格式,考慮到路徑長度一般不超過MAX_PATH長度,所以向前推MAX_PATH個位置,但是此處又要注意這一原則:RICHEDIT20A在計算字元位置時,將一個漢字看成一個字元,所以考慮到擷取的字元傳中可能包含漢字字元,所以在原有的長度基礎上增加一倍,以免呼叫GetSelText時buf溢位。獲取到字串以後,根據提示文字的格式,就可以解析出具體的路徑資訊。由於上面已給出響應超連結單擊的介面,下面接著給出解析路徑的相關程式碼:

  1. // 獲取“開啟檔案”連結前面的文字,解析出檔案的完整路徑
  2. CString CRichEditCtrlEx::GetFilepath( UINT nCurPos, BOOL bForOpenFile )  
  3. {  
  4.     UINT nTempCurPos = nCurPos;  
  5.     // 下面對檔案路徑的解析,嚴格按照提示資訊的格式進行解析,提示資訊的格式
  6.     // 為:您成功接收了檔案“E:\test-13.txt”。開啟檔案  開啟所在資料夾 
  7.     // 如果是為“開啟所在資料夾”連結解析檔案路徑,要將連結前面的“開啟檔案  ”文字給
  8.     // 偏移掉(“開啟檔案”四個漢字以及兩個英文輸入法下的空格)
  9.     if ( !bForOpenFile )   
  10.     {  
  11.         nTempCurPos -= 2*1; // 偏移兩個英文輸入法下的空格
  12.         nTempCurPos -= 4*1; // 4*1,偏移“開啟檔案”四個漢字
  13.     }  
  14.     // 考慮檔案路徑最長不超過MAX_PATH長度,所以由“開啟檔案”連結的位置,找到前MAX_PATH長度的文字
  15.     int nStartPos = 0;  
  16.     int nEndPos = 0;  
  17.     if ( nTempCurPos >= MAX_PATH*sizeof(TCHAR)+2*1 ) // 2*1表示將“開啟資料夾”連結前的"”。"兩個漢字字元去掉
  18.     {  
  19.         nStartPos = nTempCurPos - MAX_PATH*sizeof(TCHAR) - 2*1;  // 2*1,保證nStartPos大於等於0
  20.     }  
  21.     else
  22.     {  
  23.         nStartPos = 0;  
  24.     }  
  25.     nEndPos = nTempCurPos-2*1; // 去掉連結前面的"”。"兩個漢字字元
  26.     // 對於RICHEDIT2.0有點奇怪,它在計算字元的位置時,將漢字看作一個字元,所以下面在計算nBufLen時要乘以2,
  27.     // 防止buf越界
  28.     int nBufLen = (nEndPos - nStartPos + 1)*2; // 防止選中文字長度大於buf長度,導致陣列越界 
  29.     SetSel( nStartPos, nEndPos );   
  30.     TCHAR* pchText = newTCHAR[nBufLen];  
  31.     memset( pchText, 0, nBufLen );  
  32.     GetSelText( pchText );  
  33.     // 擷取的文字中可能包含多個路徑資訊,下面要找到最後一個路徑資訊,即找到最後一個_T("“")
  34.     CString strFilePath = pchText;  
  35.     int nPos = 0;  
  36.     int nTemp = strFilePath.Find( _T("“") );  
  37.     CString strTemp = strFilePath;  
  38.     while( nTemp != -1)  
  39.     {  
  40.         nPos += nTemp;  
  41.         strTemp = strTemp.Right(  strTemp.GetLength() - nTemp - 2 ); // 2表示將_T("“")字元偏移掉
  42.         nTemp = strTemp.Find( _T("“") );  
  43.         if ( nTemp != -1 )  
  44.         {  
  45.             nPos += 2; // 2表示將_T("“")字元偏移掉
  46.         }  
  47.     }  
  48.     int nLen = strFilePath.GetLength() - nPos - 2;  
  49.     strFilePath = strFilePath.Right( strFilePath.GetLength() - nPos - 2 ); // 2表示將_T("“")字元偏移掉
  50.     nLen = strFilePath.GetLength();  
  51.     delete []pchText;  
  52.     SetSel( -1, -1 );  
  53.     return strFilePath;  
  54. }  

        上面的程式碼始終要結合RICHEDIT20A在計算字元位置時,將一個漢字看成一個字元這一原則來看,看起來可能有很多感覺彆扭的地方,但為了實現我們的目標,不得不改造出上面的程式碼。

        以上就是將richedit1.0改造成2.0版本的RICHEDIT20A過程中遇到的一些細節問題,鑑於網上關於RICHEDIT20A細節處理方面的問題介紹的很少,所以在此分享出來。