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






4.67/5 (16投票s)
使用 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日