Introduction
Every so often someone will post a request to the message boards requesting help in using the Rich Edit control in a chat client. Since I've done this
before it seemed like a good topic for an article.
Background
The class I present here is from an IRC chat client I wrote in 1999/2000. It was a specialised client oriented toward trivia games and not of general interest,
though it could certainly function as a general purpose chat client. If you're really curious you can download it from my website
MindProbes and have a look. The direct link is
Prober
(disclaimer: I'm no longer involved in any way with the site).
The Rich Edit control
I'm guessing here but it seems to me that the Rich Edit control was designed to be used as an edit control replacement providing extra functionality. It assumes
the user is typing directly into it. This contrasts with what a typical chat client display window would do. The typical chat client window is read only (text
is added from an incoming stream from an IRC server) and automatically scrolls text so that the most recent entry is always visible.
Chat clients also typically allow for varied formatting of the contents based on the message type. Was this a whisper? Was it an action? Or just normal 'said' text.
(If you have no idea what I'm talking about you haven't experienced a chat room). The need to differentiate different message types makes the Rich Edit control
seem a 'natural' fit for the display. But it's not as easy as that!
More detail
So let's look at the class and pick it apart.
class CChatRichEd : public CRichEditCtrl
{
DECLARE_DYNAMIC(CChatRichEd);
public:
CChatRichEd();
virtual ~CChatRichEd();
public:
CHARFORMAT& CharFormat() { return m_cfDefault; }
public:
BOOL Create(DWORD dwStyle, const RECT& rcRect,
CWnd* pParentWnd, UINT nID);
void AppendText(LPCTSTR szText);
BOOL SaveToFile(CFile *pFile);
void Freeze();
void Thaw();
void Clear();
protected:
void InternalAppendText(LPCTSTR szText);
static DWORD CALLBACK StreamCallback(DWORD dwCookie, LPBYTE pbBuff,
LONG cb, LONG *pcb);
int m_iLineCount,
m_iLastLineCount;
CStringList m_cslDeferredText;
BOOL m_bFrozen;
CHARFORMAT m_cfDefault;
afx_msg void OnSize(UINT nType, int cx, int cy);
afx_msg void OnLink(NMHDR *in_pNotifyHeader, LRESULT *out_pResult);
DECLARE_MESSAGE_MAP()
};
The constructor does little of interest save for initialising a
CHARFORMAT
structure. This structure is used by the Rich Edit control to set formatting
for inserted text. The formatting you can set includes font settings and text colour.
The Create()
function ensures that we've done the appropriate initialisation of the Rich Edit control DLL before we try and create it.
BOOL CChatRichEd::Create(DWORD dwStyle, const RECT& rcRect,
CWnd* pParentWnd, UINT nID)
{
if (!::AfxInitRichEditEx())
return FALSE ;
CWnd* l_pWnd = this;
return l_pWnd->Create(_T("RichEdit20A"), NULL, dwStyle, rcRect,
pParentWnd, nID);
}
Hmmm, we've got a function there that's not part of the windows API.
AfxInitRichEditEx()
is defined here (in the RichEd.cpp file).
_AFX_RICHEDITEX_STATE::_AFX_RICHEDITEX_STATE()
{
m_hInstRichEdit20 = NULL ;
}
_AFX_RICHEDITEX_STATE::~_AFX_RICHEDITEX_STATE()
{
if (m_hInstRichEdit20 != NULL)
::FreeLibrary(m_hInstRichEdit20) ;
}
_AFX_RICHEDITEX_STATE _afxRichEditStateEx;
BOOL PASCAL AfxInitRichEditEx()
{
_AFX_RICHEDITEX_STATE *l_pState = &_afxRichEditStateEx;
if (l_pState->m_hInstRichEdit20 == NULL)
l_pState->m_hInstRichEdit20 = LoadLibraryA(_T("RICHED20.DLL"));
return l_pState->m_hInstRichEdit20 != NULL ;
}
Well that's pretty simple. The
RichEd.cpp
file defines a global variable of type
_AFX_RICHEDITEX_STATE
. Because it's a global the
compiler dutifully runs the constructor before our program's
WinMain
is called. The constructor loads the
RICHED20.DLL
library,
ensuring it's in our process space when we come to run the
CChatRichEd
constructor. Just to be sure, the
CChatRichEd::Create()
function
checks that the library has been loaded. (this code,
_AFX_RICHEDITEX_STATE
, was written by Andrew Forget and the article I found it on is at
CodeGuru).
So now we've created our control. As I said before, typically it's a readonly control, so the only way text can get to it is by calling the AppendText()
function. So let's have a look at that.
void CChatRichEd::AppendText(LPCTSTR szText)
{
if (m_bFrozen)
m_cslDeferredText.AddHead(szText);
else
InternalAppendText(szText);
}
Hmmmmm so now we have to know if the control is thawed or frozen? Yes we do. Picture the use of this control in a chat client. You have maybe a dozen people connected
to the chat server from half a dozen different countries. Depending on what's being said one or more users may want to scroll back and see previous history. That's
very difficult to do without telling the chat server to shut up. Since the IRC protocol doesn't support such a message we need to do it ourselves. We do this by
'freezing' the control (under user control). When the control is 'frozen' it doesn't display new messages. But we don't want to lose new
messages, just defer them. When we 'thaw' the control we want any messages recieved since we 'froze' it to be displayed. That's the purpose of the
m_cslDeferredText
string array.
The control doesn't 'freeze' or 'thaw' itself. That's the responsibility of your application.
Most of the time the m_cslDeferredText
array is empty and messages are added directly to the chat control.
So our control isn't frozen. What does InternalAppendText()
do?
void CChatRichEd::InternalAppendText(LPCTSTR szText)
{
int len;
ASSERT(szText);
ASSERT(AfxIsValidString(szText));
int iTotalTextLength = GetWindowTextLength();
CWnd *focusWnd = GetFocus();
HideSelection(TRUE, TRUE);
SetSel(iTotalTextLength, iTotalTextLength);
SetSelectionCharFormat(m_cfDefault);
ReplaceSel(szText);
len = GetWindowTextLength();
SetSel(len, len);
if (iTotalTextLength > 125000)
{
SetSel(0, 50000);
ReplaceSel(_T(""));
SetSel(iTotalTextLength, iTotalTextLength);
}
HideSelection(FALSE, TRUE);
SendMessage(EM_SCROLLCARET, 0, 0);
if (focusWnd != (CWnd *) NULL)
focusWnd->SetFocus();
}
This is also pretty simple. We need to hide the current selection (else the control does amazing things visually), select the end of the text, set the character
formatting and insert the new text. When we've inserted the text we scroll the cursor to the end of the control.
This code also sets a hard limit of 125000 bytes on the text length. If the control contains more than 125000 bytes it will delete the first 50000 bytes. Hmmmm this is
Win32 and we have gigabytes of virtual memory space available right? Right. But empirical experience (which is admittedly two years old) tells me that trying to
save more than 125000 bytes will cause some users of your chat client major problems. Tune those numbers for yourself :)
The OnLink() function
If your chat client is using Rich Edit version 2.0 or later (I think all versions of Windows later than Windows 95 original install have Rich Edit 2.0 or later)
the Rich Edit control understands URL's. If the text includes something that looks like a URL or an email address (for example http://www.codeproject.com or
mailto:ultramaroon@mindprobes.net) it will show the text matching the pattern as a link. Clicking on the link generates an
EN_LINK
message which
the class intercepts and turns into a
ShellExecute "open"
action.
ShellExecute
in turn does the 'right' thing with the URL.
Saving the chat history
I'm not responsible for your divorce :) Those who use your chat client may want to save the history. You create a
CFile
using whatever method is
appropriate and pass a pointer to it to the
SaveToFile()
function. That function in turn sets up the Rich Edit control stream conditions needed and saves
the contents as an RTF file.
Using this in your own projects
I've simplified a couple of things. The most important simplification is in the way text is formatted. In my chat client the Rich Edit control doesn't have a
default character format. The code that reads the incoming chat stream creates an object that specifies the formatting based on the incoming message type, ie, normal
chat, whispers, server messages etc. Likewise the deferred text object isn't a string list because it doesn't contain strings per se.
I was that close to including my entire object heirarchy for text objects. I decided not to because it has too many dependencies on other parts of my chat
client. How are colours specified? How are fonts specified? Suddenly a simple article becomes a treatise on my way of doing things. This is not my intention :)
So really, how do I use this in my project?
Add the
ChatRichEd.cpp/h
files to your project. When you want to use the chat control in a dialog (or in a view) add a custom control whose class is
RichEdit20A
. Set the control styles as required. The sample uses
0x50a10844
which translates into
WS_CHILDWINDOW | WS_VISIBLE | WS_OVERLAPPED | WS_BORDER | WS_VSCROLL | WS_MAXIMIZEBOX | ES_READONLY | ES_AUTOVSCROLL | ES_MULTILINE
.
You then need to create an instance of CChatRichEd
and subclass the Rich Edit control to that instance. In the sample project I did this by adding
CChatRichEd m_richEdit;
to the dialog header and
DDX_Control(pDX, IDC_RICHEDIT, m_richEdit);
to the
DoDataExchange()
function.
History
First version - December 7 2003