Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A chat control based on the Rich Edit control

0.00/5 (No votes)
6 Dec 2003 2  
Using the Rich Edit control as a chat window.

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);
// Construction

public:
                    CChatRichEd();
    virtual         ~CChatRichEd();

// Attributes

public:
    CHARFORMAT&     CharFormat()    { return m_cfDefault; }

// Operations

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();

// Overrides

    // ClassWizard generated virtual function overrides

    //{{AFX_VIRTUAL(CRichEd)

    //}}AFX_VIRTUAL


    // Generated message map functions

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(CRichEd)

    afx_msg void    OnSize(UINT nType, int cx, int cy);
    //}}AFX_MSG

    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.

//  This is the public interface for appending text to the control.  It

//  either appends it directly to the control or adds it to a string 

//  list if the control is frozen.  When the control is thawed the

//  strings are taken off the list and added to the control.

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();

    //  Hide any selection and select the end of text marker.

    HideSelection(TRUE, TRUE);
    SetSel(iTotalTextLength, iTotalTextLength);

    //  Now set the character format

    SetSelectionCharFormat(m_cfDefault);
    //  And put the text into the selection

    ReplaceSel(szText);
    len = GetWindowTextLength();
    //  Now select the end of text marker again

    SetSel(len, len);

    if (iTotalTextLength > 125000)
    {
        //  The control's starting to get full so trim off the first 

        //  50,000 bytes....

        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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here