65.9K
CodeProject 正在变化。 阅读更多。
Home

一个基于 Rich Edit 控件的聊天控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (16投票s)

2003年12月7日

CPOL

6分钟阅读

viewsIcon

154327

downloadIcon

7204

使用 Rich Edit 控件作为聊天窗口。

引言

时不时会有人在留言板上发帖,请求帮助在聊天客户端中使用 Rich Edit 控件。由于我以前做过这个,这似乎是个不错的文章主题。

背景

我在这里介绍的类来自我 1999/2000 年编写的一个 IRC 聊天客户端。它是一个专门面向知识问答游戏的客户端,不具有普遍性,但它当然可以作为一个通用的聊天客户端来使用。如果你真的好奇,可以从我的网站 MindProbes 下载并看一看。直接链接是 Prober(免责声明:我已不再以任何方式参与该网站)。

Rich Edit 控件

我猜想,Rich Edit 控件的设计初衷是作为一个提供额外功能的编辑控件替代品。它假定用户直接在其中输入。这与典型的聊天客户端显示窗口的功能形成对比。典型的聊天客户端窗口是只读的(文本从 IRC 服务器的传入流中添加),并自动滚动文本,以使最新的条目始终可见。

聊天客户端通常还允许根据消息类型对内容进行多样化的格式设置。这是一条私信吗?是一个动作吗?还是只是普通的“说”的文本。(如果你不知道我在说什么,那你还没体验过聊天室)。区分不同消息类型的需求使得 Rich Edit 控件似乎是显示窗口的“天作之合”。但事情没那么简单!

更多细节

现在让我们来看看这个类,并逐一分析。
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()
};
构造函数除了初始化一个 CHARFORMAT 结构外,没什么值得注意的。这个结构被 Rich Edit 控件用来为插入的文本设置格式。你可以设置的格式包括字体设置和文本颜色。

Create() 函数确保我们在尝试创建 Rich Edit 控件之前,已经完成了对它 DLL 的适当初始化。

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);
}
嗯,我们这里有一个不属于 Windows API 的函数。AfxInitRichEditEx() 是在这里(在 RichEd.cpp 文件中)定义的。
_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 ;
}
好吧,这很简单。RichEd.cpp 文件定义了一个类型为 _AFX_RICHEDITEX_STATE 的全局变量。因为它是全局的,编译器会在我们的程序的 WinMain 被调用之前尽职地运行其构造函数。该构造函数加载了 RICHED20.DLL 库,确保在运行 CChatRichEd 构造函数时它已经在我们的进程空间中。为了确保万无一失,CChatRichEd::Create() 函数会检查该库是否已加载。(这段代码 _AFX_RICHEDITEX_STATE 是由 Andrew Forget 编写的,我找到它的文章在 CodeGuru 上)。

现在我们已经创建了我们的控件。如我之前所说,它通常是一个只读控件,所以文本进入它的唯一方式是调用 AppendText() 函数。那么让我们来看看这个函数。

//  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);
}
嗯……所以现在我们需要知道控件是“解冻”还是“冻结”状态?是的,我们需要。想象一下在一个聊天客户端中使用这个控件的场景。可能有十几个来自六个不同国家的人连接到聊天服务器。根据正在说的内容,一个或多个用户可能想回滚查看以前的历史记录。如果不告诉聊天服务器闭嘴,这很难做到。由于 IRC 协议不支持这样的消息,我们需要自己实现。我们通过“冻结”控件(由用户控制)来做到这一点。当控件被“冻结”时,它不显示新消息。但我们不想丢失新消息,只是推迟显示它们。当我们“解冻”控件时,我们希望显示自“冻结”以来收到的任何消息。这就是 m_cslDeferredText 字符串数组的目的。

控件不会自己“冻结”或“解冻”。这是你的应用程序的责任。

大多数时候,m_cslDeferredText 数组是空的,消息直接添加到聊天控件中。

所以我们的控件没有被冻结。那么 InternalAppendText() 是做什么的?

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();
}
这也相当简单。我们需要隐藏当前的选择(否则控件在视觉上会做一些惊人的事情),选择文本的末尾,设置字符格式并插入新文本。当我们插入文本后,我们将光标滚动到控件的末尾。

这段代码还对文本长度设置了 125000 字节的硬性限制。如果控件包含超过 125000 字节,它将删除前 50000 字节。嗯……这是 Win32,我们有千兆字节的虚拟内存空间可用,对吧?对。但经验(诚然是两年前的)告诉我,尝试保存超过 125000 字节会导致你聊天客户端的某些用户出现严重问题。你自己调整这些数值吧 :)

OnLink() 函数

如果你的聊天客户端使用 Rich Edit 2.0 或更高版本(我认为所有晚于 Windows 95 初始安装的 Windows 版本都有 Rich Edit 2.0 或更高版本),Rich Edit 控件可以理解 URL。如果文本中包含看起来像 URL 或电子邮件地址的内容(例如 https://codeproject.org.cn 或 mailto:ultramaroon@mindprobes.net),它会将匹配模式的文本显示为链接。点击链接会生成一个 EN_LINK 消息,该类会拦截此消息并将其转换为一个 ShellExecute "open" 操作。ShellExecute 接着会对 URL 执行“正确”的操作。

保存聊天记录

我可不对你的离婚负责 :) 你的聊天客户端的用户可能想要保存历史记录。你可以使用任何合适的方法创建一个 CFile,并将其指针传递给 SaveToFile() 函数。该函数会设置 Rich Edit 控件所需的流条件,并将内容保存为 RTF 文件。

在您自己的项目中使用它

我简化了一些东西。最重要的简化在于文本格式化的方式。在我的聊天客户端中,Rich Edit 控件没有默认的字符格式。读取传入聊天流的代码会创建一个对象,该对象根据传入的消息类型(即,正常聊天、私聊、服务器消息等)指定格式。同样,延迟文本对象不是一个字符串列表,因为它本身不包含字符串。

差一点就把我整个文本对象的对象层次结构都包含进来了。我决定不这样做,因为它对我的聊天客户端的其他部分有太多的依赖。颜色是如何指定的?字体是如何指定的?突然之间,一篇简单的文章变成了一篇关于我做事方式的专著。这不是我的本意 :)

所以,我到底该如何在我的项目中使用它?

ChatRichEd.cpp/h 文件添加到你的项目中。当你想在对话框(或视图)中使用聊天控件时,添加一个类名为 RichEdit20A 的自定义控件。根据需要设置控件样式。示例使用了 0x50a10844,这转换成 WS_CHILDWINDOW | WS_VISIBLE | WS_OVERLAPPED | WS_BORDER | WS_VSCROLL | WS_MAXIMIZEBOX | ES_READONLY | ES_AUTOVSCROLL | ES_MULTILINE

然后你需要创建一个 CChatRichEd 的实例,并将 Rich Edit 控件子类化到该实例。在示例项目中,我是通过添加

CChatRichEd m_richEdit;
到对话框头文件
DDX_Control(pDX, IDC_RICHEDIT, m_richEdit);
DoDataExchange() 函数来实现的。

历史

第一版 - 2003年12月7日

© . All rights reserved.