显示位图和其他OLE对象的富文本控件






4.90/5 (65投票s)
2005 年 2 月 10 日
13分钟阅读

381768

18601
COleRichEditCtrl 可以显示 RTF 文本以及位图、视频片段、Word、Excel 和 PowerPoint 文档以及任何其他 OLE 对象。
- 下载 COleRichEditCtrl 的演示项目 - 461 Kb (包含一个发布版可执行文件,无需编译即可运行)。
- 仅 COleRichEditCtrl 的源代码 - 3.36 Kb
目录
- 引言
COleRichEditCtrl
类- 演示程序
- 开发
COleRichEditCtrl
类过程中遇到的一个问题 - 如何在项目中 OLERichEditCtrl 控件
- 一些最后的话
- 版本和修订历史
- 参考文献
引言
我需要一个能够显示位图的富文本控件。在网上搜索时,我发现其他人也面临着同样的需求。对我而言,我想要一个“轻量级”的帮助功能,可以将其嵌入为 RTF 资源并在我的应用程序中显示,这样我就不需要一个功能齐全的帮助伴侣。没有截图的帮助文件有什么用呢——因此,在富文本控件中需要位图。
同样在网上搜索也让我确信,解决方案并不容易找到。偶然间,我在 Stephane Lesage 发表的一篇文章(即不是文章本身)的**评论**中找到了让一切奏效的线索,他在 此处发表的文章标题为“通过 StreamIn/ClipBoard/Drag'n'Drop 操作获取图像”。
Lesage 先生的引述
“如果你希望对象插入操作能在你的 RichEdit 控件中工作,你必须提供一个
IRichEditOleCallback
接口并实现GetNewStorage
方法。”
OLE 是所需的线索,并且根据 Lesage 先生的代码建议,我能够编写 COleRichEditCtrl
类,该类继承自 MFC 的 CRichEditCtrl
类。
COleRichEditCtrl 类
该类的代码实际上很简单。在头文件中,我定义了一个嵌套类(实际上,它不是一个真正的 class
;它是一个 interface
),它继承自 IRichEditOleCallback
,该接口在 MSDN 上有文档说明。此嵌套类中实现的最重要的方法是 GetNewStorage
,它为从剪贴板粘贴或从 RTF 流读取的新对象提供存储。这是 COleRichEditCtrl
类的头文件,为简化起见,大部分 ClassWizard 样板代码已删除。
#include <richole.h> ///////////////////////////////////////////////////////////////////////////// // COleRichEditCtrl window class COleRichEditCtrl : public CRichEditCtrl { // Construction public: COleRichEditCtrl(); virtual ~COleRichEditCtrl(); long StreamInFromResource(int iRes, LPCTSTR sType); protected: static DWORD CALLBACK readFunction(DWORD dwCookie, LPBYTE lpBuf, // the buffer to fill LONG nCount, // number of bytes to read LONG* nRead); // number of bytes actually read interface IExRichEditOleCallback; // forward declaration (see below in this header file) IExRichEditOleCallback* m_pIRichEditOleCallback; BOOL m_bCallbackSet; interface IExRichEditOleCallback : public IRichEditOleCallback { public: IExRichEditOleCallback(); virtual ~IExRichEditOleCallback(); int m_iNumStorages; IStorage* pStorage; DWORD m_dwRef; virtual HRESULT STDMETHODCALLTYPE GetNewStorage(LPSTORAGE* lplpstg); virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, void ** ppvObject); virtual ULONG STDMETHODCALLTYPE AddRef(); virtual ULONG STDMETHODCALLTYPE Release(); virtual HRESULT STDMETHODCALLTYPE GetInPlaceContext(LPOLEINPLACEFRAME FAR *lplpFrame, LPOLEINPLACEUIWINDOW FAR *lplpDoc, LPOLEINPLACEFRAMEINFO lpFrameInfo); virtual HRESULT STDMETHODCALLTYPE ShowContainerUI(BOOL fShow); virtual HRESULT STDMETHODCALLTYPE QueryInsertObject(LPCLSID lpclsid, LPSTORAGE lpstg, LONG cp); virtual HRESULT STDMETHODCALLTYPE DeleteObject(LPOLEOBJECT lpoleobj); virtual HRESULT STDMETHODCALLTYPE QueryAcceptData(LPDATAOBJECT lpdataobj, CLIPFORMAT FAR *lpcfFormat, DWORD reco, BOOL fReally, HGLOBAL hMetaPict); virtual HRESULT STDMETHODCALLTYPE ContextSensitiveHelp(BOOL fEnterMode); virtual HRESULT STDMETHODCALLTYPE GetClipboardData(CHARRANGE FAR *lpchrg, DWORD reco, LPDATAOBJECT FAR *lplpdataobj); virtual HRESULT STDMETHODCALLTYPE GetDragDropEffect(BOOL fDrag, DWORD grfKeyState, LPDWORD pdwEffect); virtual HRESULT STDMETHODCALLTYPE GetContextMenu(WORD seltyp, LPOLEOBJECT lpoleobj, CHARRANGE FAR *lpchrg, HMENU FAR *lphmenu); }; public: // Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(COleRichEditCtrl) protected: virtual void PreSubclassWindow(); //}}AFX_VIRTUAL // Implementation public: // Generated message map functions protected: //{{AFX_MSG(COleRichEditCtrl) afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); //}}AFX_MSG DECLARE_MESSAGE_MAP() };
在类的 OnCreate
处理程序中(不完全准确:请参见下面我描述的开发过程中遇到的一个问题),会从堆中分配一个 IExRichEditOleCallback
对象。对其 GetNewStorage
方法的实现遵循了我在其他地方找到的一些示例,并且确实是专门用于该任务的各种 API 的教科书式用法。
HRESULT STDMETHODCALLTYPE COleRichEditCtrl::IExRichEditOleCallback::GetNewStorage(LPSTORAGE* lplpstg) { m_iNumStorages++; WCHAR tName[50]; swprintf(tName, L"REOLEStorage%d", m_iNumStorages); HRESULT hResult = pStorage->CreateStorage(tName, STGM_TRANSACTED | STGM_READWRITE | STGM_SHARE_EXCLUSIVE | STGM_CREATE , 0, 0, lplpstg ); if (hResult != S_OK ) { ::AfxThrowOleException( hResult ); } return hResult; }
最后,由于我的目的是通过流式传输存储在可执行文件中的资源中的 RTF 流来使用该类,因此我提供了一个 StreamInFromResource
成员函数,以及一个在 EDITSTREAM
结构中使用的静态作用域的回调函数。
long COleRichEditCtrl::StreamInFromResource(int iRes, LPCTSTR sType) { HINSTANCE hInst = AfxGetInstanceHandle(); HRSRC hRsrc = ::FindResource(hInst, MAKEINTRESOURCE(iRes), sType); DWORD len = SizeofResource(hInst, hRsrc); BYTE* lpRsrc = (BYTE*)LoadResource(hInst, hRsrc); ASSERT(lpRsrc); CMemFile mfile; mfile.Attach(lpRsrc, len); EDITSTREAM es; es.pfnCallback = readFunction; es.dwError = 0; es.dwCookie = (DWORD) &mfile; return StreamIn( SF_RTF, es ); } /* static */ DWORD CALLBACK COleRichEditCtrl::readFunction(DWORD dwCookie, LPBYTE lpBuf, // the buffer to fill LONG nCount, // number of bytes to read LONG* nRead) // number of bytes actually read { CFile* fp = (CFile *)dwCookie; *nRead = fp->Read(lpBuf,nCount); return 0; }
这个架构给我带来了一个惊人的(对我而言)好处。当我开始时,我的唯一目标是显示一个位图,结果我得到了一个可以显示**任何** OLE 对象的控件。包含完全任意对象的复合文档可以正常工作:位图、视频和音频剪辑、Office 文档(Word、Excel、PowerPoint)等。还可以包含其他内容(如 PDF 文件和 HTML 文件),并且双击内容的图标即可启动内容,但除非 OLE 服务器应用程序已编写并配置为进行 OLE 就地显示,否则将不会就地显示这些对象。
演示程序
演示程序是一个不起眼的 MFC SDI 文档/视图应用程序外壳,它的唯一目的是启动一个包含 COleRichEditCtrl
的对话框。假设对话框包含帮助类型的信息,左键单击将无模式地启动对话框,以便用户仍然可以与主应用程序进行交互。对话框的代码使用一种简单的技术,涉及一个 BOOL
标志,该标志在 OnPostNcDestroy
中进行测试,以便删除为对话框对象分配的内存;但由于这不是本文的重点,因此如果你有兴趣,可以查看 CRichEditHelpDialog
类的代码。右键单击将以更熟悉的模式启动对话框,以便你看到区别。
运行程序,然后单击视图中的任意位置,以启动带有 COleRichEditCtrl
的帮助对话框。控件的内容是从可执行文件的一部分 RTF 资源流式传输进来的;内容包括标准的 RTF 文本、一个位图、一个视频剪辑、一个 Excel 电子表格和一个 PowerPoint 演示文稿。如果你看不到一个或多个这些对象,那么你可能没有安装相应的程序。
COleRichEditCtrl 类开发过程中的一个问题
我认为我已完成了 COleRichEditCtrl
类的开发,并且几乎完成了本文的撰写,当我遇到一个绊脚石,把我送回了编码台。如果你愿意,可以跳过这部分,直接进入在你的项目中使用该类,点击此处。
问题根源在于该类包装了一个控件。因为它是一个控件,所以该类必须预期它的 Windows 窗口将通过两种方式之一创建:通过显式调用 ::CreateWindow
(或 ::CreateWindowEx
,它们都由所有 CWnd
派生类的 CWnd::Create
成员函数包装),或者在调用 ::CreateDialogParam
时由 Windows 自动创建,该函数根据程序资源中的模板构建对话框。后者更常见(事实上,下一节描述如何使用该类的方式就是这样),但前者也经常使用(事实上,演示项目中使用的方法就是这样)。
为了实现 OLE 功能,控件在几乎所有其他事情发生之前就需要 OLE 回调函数。因此,当我最初编写包装类时,我将 SetOLECallback
的调用放在 COleRichEditCtrl::OnCreate
处理程序中,因为该处理程序在控件收到 Windows 发送的第一个消息之一时被调用。这是一个愚蠢的疏忽,我本应知道得更多,因为它只处理第一种窗口创建方式(即通过 ::CreateWindow
),而不处理第二种方式(即从对话框资源模板创建)。
适应这两种窗口创建方式的正确方法是众所周知的:将这种早期初始化放在 CWnd::PreSubclassWindow
中。PreSubclassWindow
函数是一个虚拟函数,在 MFC 框架将 Windows 窗口子类化为 C++ CWnd
对象之前被调用,无论窗口是通过第一种还是第二种方法创建的,它都会被调用。Paul Dilascia 在 2002 年 3 月的 MSDN 杂志 C++ Q&A 专栏中对此进行了详细讨论,请参阅 MSDN。因此,我将所有 SetOLECallback
代码移到了一个新的 COleRichEditCtrl::PreSubclassWindow
处理程序中。
这时我遇到了真正的问题。因为虽然通过对话框模板创建现在运行良好,但通过 CreateWindow
创建却不行。调用 SetOLECallback
返回一个错误代码,表示调用失败,OLE 功能确实已损坏。我追踪了代码,但无法找到失败的原因, diligent 的网络搜索也没有发现任何结果。
我推测问题与过早调用 SetOLECallback
有关,我认为当在对话框模板创建后通过 PreSubclassWindow
调用 SetOLECallback
时,富文本控件的 Windows 窗口已准备好接收消息,而通过 CreateWindow
创建后,它可能尚未准备好。因此,我寻找延迟调用 SetOLECallback
的方法。我寻找 MFC 框架早期调用的其他函数(未找到),并考虑为 WM_NCCREATE
消息安装一个 OnNcCreate
处理程序(实际上在 WM_CREATE
消息之前发送),或者 PostMessage 我的自定义消息。这些方法看起来都不太对。
最终,我在类中添加了一个 BOOL
标志来捕获 PreSubclassWindow
中的 SetOLECallback
调用结果。然后,我保留了 OnCreate
处理程序,并在其中测试了标志,以查看 SetOLECallback
是否已经从 PreSubclassWindow
中成功调用。如果没有,我只是再次调用它。这是代码。
int COleRichEditCtrl::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CRichEditCtrl::OnCreate(lpCreateStruct) == -1) return -1; // m_pIRichEditOleCallback should have been created in PreSubclassWindow ASSERT( m_pIRichEditOleCallback != NULL ); // set the IExRichEditOleCallback pointer if it wasn't set // successfully in PreSubclassWindow if ( !m_bCallbackSet ) { SetOLECallback( m_pIRichEditOleCallback ); } return 0; } void COleRichEditCtrl::PreSubclassWindow() { // base class first CRichEditCtrl::PreSubclassWindow(); m_pIRichEditOleCallback = NULL; m_pIRichEditOleCallback = new IExRichEditOleCallback; ASSERT( m_pIRichEditOleCallback != NULL ); m_bCallbackSet = SetOLECallback( m_pIRichEditOleCallback ); }
我对解决方案或问题解释不完全满意,但它确实有效。如果有人遇到过这种行为,并且知道原因或有其他解决方案,请告知我们。
如何在项目中 OLERichEditCtrl 控件
这些说明适用于 VC++ 6.0 版本,但很容易用于 .NET 等其他版本。
要将该控件用于你的项目,请将源代码和头文件(即 COleRichEditCtrl.cpp 和 COleRichEditCtrl.h)下载到一个方便的文件夹,然后将它们都包含到你的项目中(“项目”->“添加到项目”->“文件...”)。
创建你的对话框资源模板,并使用控件工具栏添加一个标准的富文本控件。
打开刚添加的富文本控件的“属性”窗口,在“样式”选项卡下,选择“多行”、“垂直滚动”和“换行”,然后取消选择“自动水平滚动”。这些是控件最常见用法的典型样式,但如果你对结果不完全满意,可能想尝试一下。例如,你可能还想要“只读”样式。
现在,我们将向你的对话框添加一个类型为 COleRichEditCtrl
的成员变量。(请参见脚注 1。)打开 ClassWizard 并选择对应于你的对话框的类。然后,添加一个类型为 CRichEditCtrl
的“控件”样式变量,它是 COleRichEditCtrl
的基类。你应该会看到类似此截图的内容。
点击所有地方的“确定”退出 ClassWizard,然后手动编辑你的对话框类以替换实际目标类 COleRichEditCtrl
。方法如下。
首先,打开对话框类的头文件,并在顶部添加 #include "OleRichEditCtrl.h"
。如果你将 COleRichEditCtrl.cpp 和 COleRichEditCtrl.h 文件添加到了与主项目不同的文件夹中,则还需要指定文件夹名称,如下所示:#include "../components/OleRichEditCtrl.h"
。然后,要使用 OLE 富文本控件而不是标准的富文本控件,请在头文件中向下滚动页面,直到看到一行如下所示的内容。
CRichEditCtrl m_ctlRichEdit;
并将其替换为以下内容:
COleRichEditCtrl m_ctlRichEdit;
在构建和运行程序之前,你**必须**确保它在某处调用了 AfxInitRichEdit()
,通常是在 CWinApp::InitInstance()
调用中。如果你在创建应用程序时没有选择“复合文档支持”选项,那么你必须手动编辑代码以插入对 AfxInitRichEdit()
的调用。现在就做。现在构建并运行你的程序。
要将 RTF 文本包含为程序资源的一部分,请使用像标准 WordPad 这样的“简易”RTF 编辑器创建文档。(我推荐 WordPad 这样的简单 RTF 编辑器,因为像 Word 这样复杂的编辑器经常会在 RTF 流中插入大量不必要且混乱的标签。)将文档以 RTF 格式保存,然后将它的**副本**移动到项目中的 /res 文件夹。我建议使用副本而不是直接保存,因为 Visual Studio 有一个讨厌的习惯,如果你不小心,它会擦除自定义资源的全部内容(这非常令人沮丧,相信我)。假设文件名为 text.rtf。转到 ClassView 的“资源”选项卡,右键单击项目,然后从弹出菜单中选择“导入”资源。
选择你的 text.rtf 文件,然后为你的自定义资源定义一个字母“类型”。我倾向于使用明显的东西,比如“RTF_TEXT
”,如下图所示。
现在,要在对话框打开时流式传输此资源,只需使用 COleRichEditCtrl::StreamInFromResource
函数。在你的对话框的 OnInitDialog()
函数中,只需添加以下一行代码:
m_ctlRichEdit.StreamInFromResource( IDR_RTF_TEXT1, "RTF_TEXT" );
就是这样:构建并运行你的程序。你可能需要执行“重新生成所有”才能将自定义资源构建到你的可执行文件中,因为 Visual Studio 在检测到非标准资源(如你的“RTF_TEXT
”资源)已添加到项目中时并不很擅长。
一些最后的话
如果你在使用该类时遇到麻烦,这里有一些实用的建议。
- 如果你的程序在添加
COleRichEditCtrl
之前运行正常,但之后似乎根本不工作,那么你可能需要插入对AfxInitRichEdit()
的调用。最佳位置是在应用程序的CMyApp::InitInstance
函数中。 - 如果你添加了一个自定义的“
RTF_TEXT
”资源,如上面提到的虚构的 test.rtf 文件,但在构建后,你在控件中看不到资源的内容,那么你可能需要执行“重新生成所有”。Visual Studio 在检测到非标准资源(如你的“RTF_TEXT
”资源)已添加到项目中时并不很擅长。 - 我在演示程序中注意到了一些奇怪的行为,即如果以特定的顺序调整大小和滚动,富文本控件似乎不会正确地擦除和重绘自身。在演示中,对话框有一个可调整大小的边框,可以通过拖动边框以传统方式调整大小。控件本身也有一个垂直滚动条用于垂直滚动内容。如果你启动对话框然后调整对话框大小而不触摸滚动条,你就会看到奇怪的行为,特别是如果你将其调整为较窄的宽度然后再次将其变宽(这会使控件计算新的换行位置)。一旦你触摸滚动条至少一次,之后一切都会正常。换句话说,一旦你滚动一次,你就可以随意调整大小,控件将完美地擦除和重绘自身。我不知道这种行为的原因,而且我的网络搜索也没有发现任何结果。如果有人有修复方法,请告知我们。
版本和修订历史
- 2005 年 2 月 4 日 - 首次发布。
参考文献
这里,集中列出了文章中提到的所有文章和链接。
- Stephane Lesage,“通过 StreamIn/ClipBoard/Drag'n'Drop 操作获取图像”。
- MSDN,“IRichEditOleCallback 接口”。
- Paul Dilascia,“C++ Q&A: CWnd 控件中的 PreSubclassWindow”,2002 年 3 月的 MSDN 杂志。
脚注
- 此时,你真的应该能够使用 ClassWizard 直接添加
COleRichEditCtrl
类型的变量,而无需执行正文中的步骤。但是,我发现 ClassWizard 在将其添加到数据库时存在问题。你可以尝试以下过程来强制 ClassWizard 重新构建其数据库,尽管这对我过去处理其他控件时有效,但我刚刚尝试过,但由于某种原因无效。正文中的过程始终有效,但万一这个替代过程对你有效,这里是:类数据库存储在项目文件夹中扩展名为“.clw”的文件中。要强制 ClassWizard 重新构建类数据库,请使用 Explorer 打开你的项目工作区,并找到扩展名为“.clw”的文件。删除它。(相信我,或者如果你不信,将其重命名为“.clw~1”等扩展名。)现在,打开 ClassWizard,你将收到一条消息,说“.clw”文件不存在,并询问你是否要从源文件中重新构建它。当然你应该选择“是”。在出现的对话框中,选择“全部添加”。此外,还要确保还将 COleRichEditCtrl.cpp 和 COleRichEditCtrl.h 添加到你存储它们的任何文件夹中。