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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (65投票s)

2005 年 2 月 10 日

13分钟阅读

viewsIcon

381768

downloadIcon

18601

COleRichEditCtrl 可以显示 RTF 文本以及位图、视频片段、Word、Excel 和 PowerPoint 文档以及任何其他 OLE 对象。

Bitmaps and other OLE objects displayed in a rich edit control

目录

引言

我需要一个能够显示位图的富文本控件。在网上搜索时,我发现其他人也面临着同样的需求。对我而言,我想要一个“轻量级”的帮助功能,可以将其嵌入为 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.cppCOleRichEditCtrl.h)下载到一个方便的文件夹,然后将它们都包含到你的项目中(“项目”->“添加到项目”->“文件...”)。

创建你的对话框资源模板,并使用控件工具栏添加一个标准的富文本控件。

Using the control toolbar to add a rich edit control

打开刚添加的富文本控件的“属性”窗口,在“样式”选项卡下,选择“多行”、“垂直滚动”和“换行”,然后取消选择“自动水平滚动”。这些是控件最常见用法的典型样式,但如果你对结果不完全满意,可能想尝试一下。例如,你可能还想要“只读”样式。

Setting styles of the rich edit

现在,我们将向你的对话框添加一个类型为 COleRichEditCtrl 的成员变量。(请参见脚注 1。)打开 ClassWizard 并选择对应于你的对话框的类。然后,添加一个类型为 CRichEditCtrl 的“控件”样式变量,它是 COleRichEditCtrl 的基类。你应该会看到类似此截图的内容。

Screenshot of ClassWizard, showing how to add a member variable of type CRichEditCtrl

点击所有地方的“确定”退出 ClassWizard,然后手动编辑你的对话框类以替换实际目标类 COleRichEditCtrl。方法如下。

首先,打开对话框类的头文件,并在顶部添加 #include "OleRichEditCtrl.h"。如果你将 COleRichEditCtrl.cppCOleRichEditCtrl.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 的“资源”选项卡,右键单击项目,然后从弹出菜单中选择“导入”资源。

Importing a custom resource

选择你的 text.rtf 文件,然后为你的自定义资源定义一个字母“类型”。我倾向于使用明显的东西,比如“RTF_TEXT”,如下图所示。

Defining the alphabetic type of the resource

现在,要在对话框打开时流式传输此资源,只需使用 COleRichEditCtrl::StreamInFromResource 函数。在你的对话框的 OnInitDialog() 函数中,只需添加以下一行代码:

m_ctlRichEdit.StreamInFromResource( IDR_RTF_TEXT1, "RTF_TEXT" );

就是这样:构建并运行你的程序。你可能需要执行“重新生成所有”才能将自定义资源构建到你的可执行文件中,因为 Visual Studio 在检测到非标准资源(如你的“RTF_TEXT”资源)已添加到项目中时并不很擅长。

顶部

一些最后的话

如果你在使用该类时遇到麻烦,这里有一些实用的建议。

  1. 如果你的程序在添加 COleRichEditCtrl 之前运行正常,但之后似乎根本不工作,那么你可能需要插入对 AfxInitRichEdit() 的调用。最佳位置是在应用程序的 CMyApp::InitInstance 函数中。
  2. 如果你添加了一个自定义的“RTF_TEXT”资源,如上面提到的虚构的 test.rtf 文件,但在构建后,你在控件中看不到资源的内容,那么你可能需要执行“重新生成所有”。Visual Studio 在检测到非标准资源(如你的“RTF_TEXT”资源)已添加到项目中时并不很擅长。
  3. 我在演示程序中注意到了一些奇怪的行为,即如果以特定的顺序调整大小和滚动,富文本控件似乎不会正确地擦除和重绘自身。在演示中,对话框有一个可调整大小的边框,可以通过拖动边框以传统方式调整大小。控件本身也有一个垂直滚动条用于垂直滚动内容。如果你启动对话框然后调整对话框大小而不触摸滚动条,你就会看到奇怪的行为,特别是如果你将其调整为较窄的宽度然后再次将其变宽(这会使控件计算新的换行位置)。一旦你触摸滚动条至少一次,之后一切都会正常。换句话说,一旦你滚动一次,你就可以随意调整大小,控件将完美地擦除和重绘自身。我不知道这种行为的原因,而且我的网络搜索也没有发现任何结果。如果有人有修复方法,请告知我们。

顶部

版本和修订历史

  • 2005 年 2 月 4 日 - 首次发布。

顶部

参考文献

这里,集中列出了文章中提到的所有文章和链接。

顶部

脚注

  1. 此时,你真的应该能够使用 ClassWizard 直接添加 COleRichEditCtrl 类型的变量,而无需执行正文中的步骤。但是,我发现 ClassWizard 在将其添加到数据库时存在问题。你可以尝试以下过程来强制 ClassWizard 重新构建其数据库,尽管这对我过去处理其他控件时有效,但我刚刚尝试过,但由于某种原因无效。正文中的过程始终有效,但万一这个替代过程对你有效,这里是:类数据库存储在项目文件夹中扩展名为“.clw”的文件中。要强制 ClassWizard 重新构建类数据库,请使用 Explorer 打开你的项目工作区,并找到扩展名为“.clw”的文件。删除它。(相信我,或者如果你不信,将其重命名为“.clw~1”等扩展名。)现在,打开 ClassWizard,你将收到一条消息,说“.clw”文件不存在,并询问你是否要从源文件中重新构建它。当然你应该选择“是”。在出现的对话框中,选择“全部添加”。此外,还要确保还将 COleRichEditCtrl.cppCOleRichEditCtrl.h 添加到你存储它们的任何文件夹中。
© . All rights reserved.