在Windowless RichEdit中插入OLE对象
RichEdit支持在文本中插入OLE对象,OLE对象中可以显示任意自定义的内容,从而达到丰富的显示效果,例如文字和图片在同一行中混合显示。本文在《实现一个可编辑的Windowless RichEdit》一本的基础上,介绍如何在Windowless RichEdit中插入OLE对象。完整的示例代码可以在 WindowlessRichEdit-Example 获得。
实现自定义的OLE对象
在插入OLE对象之前,我们首先要实现一个自定义的OLE对象。这个对象必须实现以下两个COM接口:
IOleObject
,所有OLE对象都需要实现的基础接口。IViewObject
,用于显示OLE对象的接口。
IOleObject
有不少虚函数需要实现,但其实所有函数都可以不实现,全都返回 E_NOTIMPL
也是可以的。在本文的例子中,OLE对象仅用于显示,没有任何行为,所以对于IOleObject
的所有函数都返回了 E_NOTIMPL
。
IViewObject
只有一个 Draw()
函数需要实现,用来绘制OLE对象的内容。其它函数都可以不实现,返回 E_NOTIMPL
即可。 Draw()
的参数列表比较长:
1 | HRESULT Draw( |
大部分参数用于复杂的OLE对象或者特殊的绘制场景(例如打印机),对于简单的OLE对象,通常只需要关注 hdcDraw
和 lprcBounds
这两个参数就可以了。hdcDraw
表示用于绘制的设备句柄, lprcBounds
表示OLE对象在 hdcDraw
中的绘制区域,我们只能在这个区域中进行绘制。
在这个示例的OLE对象中,我们只是简单地将OLE对象填充成蓝色,以显示它的存在。
1 | class MyOLEObject : public IOleObject, public IViewObject { |
注意, lprcBounds
的类型是 RECTL
指针,而 FillRect
的参数类型是 RECT
指针,虽然两者的类型不一样,但内存布局实际上是一样的,所以可以直接将 lprcBounds
转成 RECT
指针。
此外,我们要给这个OLE对象定义一个类ID,在插入的时候要用到:
1 | constexpr GUID CLSID_MyOLEObject = { |
插入OLE对象
接下来,就可以往RichEdit中插入这个OLE对象了。插入OLE对象大致上需要两步:
- 给RichEdit发送
EM_GETOLEINTERFACE
消息,获取IRichEditOle
接口,这个接口提供了一系列用于操作OLE对象的函数。 - 调用
IRichEditOle
的InsertObject()
函数,插入OLE对象。
InsertObject
的参数是一个 REOBJECT
结构,我们需要填充这个结构的字段:
cbStruct
,结构的大小,填sizeof(REOBJECT)
即可。cp
,OLE对象的插入位置,填0插入到文本第一个字符的位置,填1插入到第二字符的位置,以此类推。可以填常量REO_CP_SELECTION
,表示插入到当前光标选中的位置。clsid
,OLE对象的类ID,填上文中定义的CLSID_MyOLEObject
。poleobj
,要插入的OLE对象的指针。注意,OLE对象插入后,RichEdit会调用AddRef()
来增加它的引用计数,所以在插入之后我们要调用Release()
来减少引用计数,避免内存泄露。pstg
,IStorage
接口的指针,一般不需要设置,传nullptr
即可。polesite
,IOleClientSite
接口的指针,用于OLE对象与其所在的容器进行交互。调用IRichEditOle
的GetClientSite()
函数获取这个指针并传入即可。sizel
,OLE对象的大小,注意这个大小是以HIMETRIC为单位,即0.01毫米。可以使用AtlPixelToHiMetric()
函数将像素大小转换成HIMETRIC大小。dvaspect
,使用哪个“外表”来绘制OLE对象,一个OLE对象可能有多个外表,例如缩略图、图标等。普通的OLE对象通常只有一个默认的外表,填DVASPECT_CONTENT
即可。dwFlags
,OLE对象的一些属性设置。常用的有两个:REO_BELOWBASELINE
表示OLE对象的底部与文字的底部对齐。如果没有这个设置,OLE对象的底部会与文字基线对齐,这会导致OLE对象的位置偏上。REO_OWNERDRAWSELECT
表示OLE对象的选中态由自己来绘制。如果没有这个设置,OLE对象在被选中的时候,会以反色来显示(如白变黑),这通常不是我们想要的效果,所以要加上这个设置,我们自己来绘制被选中时的样式。
dwUser
,一个自定义的数值,跟当前插入的OLE对象绑定,一般用不到。
以下是插入OLE对象的代码片段:
1 | //从TextService取出IRichEditOle接口 |
判断OLE对象是否被选中
我们在插入OLE对象的时候指定了 REO_OWNERDRAWSELECT
设置,意味着我们需要自己来绘制OLE对象的选中态。现在,不论该OLE对象是否被选中,都显示同样的蓝色,没有区分度。所以,接下来要修改 Draw()
函数的实现,在OLE对象被选中时显示不同的颜色。
RichEdit没有任何接口或者通知可以直接告诉我们OLE对象是否被选中,所以我们只能自己去判断。判断的方法如下:
- 给RichEdit发送
EM_EXGETSEL
消息,获取当前选中的范围。 - 调用
IRichEditOle
的GetObjectCount()
和GetObject()
函数,遍历取出每一个OLE对象的位置,如果遍历到的OLE对象就是当前对象,再看看是否在选中的范围中。
以下是判断OLE对象是否被选中的函数:
1 | bool MyOLEObject::IsSelected() const { |
IRichEditOle
的 GetObject()
也需要传入 REOBJECT
结构,不过此时只需要设置 cbStruct
字段即可,其它字段都由该函数来填充。调用成功后, REOBJECT
的 cp
字段表示OLE对象在文本中的位置,将其与选中范围比较即可知道它是否被选中了。如果文本被全选了, CHARRANGE
的 cpMin
是0, cpMax
是-1,所以比较的时候要区分两种场景。
虽然 REOBJECT
的 dwFlags
字段有一个 REO_SELECTED
的值,似乎可以用来判断OLE对象是否被选中,但这个值只有在OLE对象被单独选中的时候才会设置,如果选中的是一段文字和OLE对象,则不会设置,所以不能依赖这个值。
调用 GetObject()
的时候,最后一个参数传入 REO_GETOBJ_POLEOBJ
, REOBJECT
中才会填充 poleobj
字段。这个指针是增加了引用计数的,所以用完之后我们要手动地减少它的引用计数。在上面的代码中,使用了 CComPtr
来自动释放。
以下是修改后的 Draw()
函数实现:
1 | HRESULT MyOLEObject::Draw( |
修改鼠标光标的样式
OLE对象并不是文字,但RichEdit在某些时候仍然会将它当做文字来对待。例如,当鼠标移动到OLE对象之上时,光标仍然会显示成 I
样式,但在大部分情况下我们希望它显示成默认的箭头样式。
为了修改鼠标光标在OLE对象上的样式,我们要在处理 WM_SETCURSOR
消息,在调用 ITextServices
的 OnTxSetCursor()
之前(详情可参考《实现一个可编辑的Windowless RichEdit》),判断鼠标是否位于OLE对象之上。判断的方法如下:
- 调用
IRichEditOle
的QueryInterface()
函数,取出它的ITextDocument
接口。 - 调用
ITextDocument
的RangeFromPoint()
函数,根据鼠标位置取出最接近的文本,返回的是ITextRange
接口。 - 调用
ITextRange
的GetEmbeddedObject()
函数,取出OLE对象。如果返回空指针,则说明鼠标不在OLE对象上,反之则在OLE对象上。
以下是获取鼠标位置之下的OLE对象的函数:
1 | CComPtr<MyOLEObject> GetOLEObjectAtMouseCursor() { |
ITextDocument
的 RangeFromPoint()
函数使用的是屏幕坐标,调用 GetCursorPos()
来拿鼠标位置刚刚好。此外,RichEdit会调用 ITextHost
的 TxScreenToClient()
函数来将屏幕坐标转换成客户区坐标,所以我们也要实现这个函数,简单地调用 ScreenToClient()
即可:
1 | BOOL MyTextHost::TxScreenToClient(LPPOINT lppt) { |
要注意,RangeFromPoint()
函数在旧版本的RichEdit中存在bug,取到的文本不准。具体表现是:当坐标位于OLE对象的后半部分时,取到的是下一个位置的文本;只有坐标位于OLE对象的前半部分时才能取到正确的文本。最新的RichEdit 4.0没有这个问题,所以建议尽可能使用新版本的RichEdit。只要在调用 LoadLibrary()
的时候指定 msftedit.dll
即可使用最新版本的RichEdit:
1 | //使用最新版本的RichEdit |
可参考 About Rich Edit Controls 以获取更多关于RichEdit版本的信息。