Introduction
This article shows how you can insert images, controls and ActiveX objects into a .NET RichTextBox
control by using the OLE way, like explained in the Microsoft site. Unfortunately, it covers only the sample with a C++ source code, so I need to implement a similar solution in managed code (C#).
There are other related articles for inserting images and OLE objects into a RichTextBox
, but they are using RTF codes, and I need a more specialized control suitable to be used for chat and to provide a way to insert emoticons, progress bars and images, and finally, recover them by getting their OLE handles or any object attribute.
Special thanks to Khendys Gordon for the article: "Insert Plain Text and Images into RichTextBox at Runtime" and John Fisher for his article: "Use IRichEditOle from C#".
Background
To achieve the solution, I need to use the P/Invoke (Platform Invoke) methods. I got a lot of information from pinvoke.net.
The first step to insert an OLE object into a RichTextBox
is to get its IRichEditOle
interface. It could be done, by sending the message EM_GETOLEINTERFACE
to the control:
this.IRichEditOle = SendMessage(richEditHandle, EM_GETOLEINTERFACE, 0);
With this interface, you can insert objects through the REOBJECT
struct
. It is important to note the you can specify the insertion point, the aspect and the dwUser
variable to store flags or any related information for this object, so you can recover it at any time for update.
REOBJECT reoObject=new REOBJECT();
reoObject.cp = this._richEdit.TextLength;
reoObject.clsid = guid;
reoObject.pstg = pStorage;
reoObject.poleobj = Marshal.GetIUnknownForObject(control);
reoObject.polesite = pOleClientSite;
reoObject.dvAspect = (uint)(DVASPECT.DVASPECT_CONTENT);
reoObject.dwFlags = (uint)(REOOBJECTFLAGS.REO_BELOWBASELINE);
reoObject.dwUser = 1;
this.IRicEditOle.InsertObject(reoObject);
For inserting images, you need to implement the interface IDataObject
, in this case I named it myDataObject
. This class is an OLE callback object that uses the FORMATETC
and the STGMEDIUM
structures to display the image, telling to the OLE container that this object is a GDI medium (TYMED_GDI
) with a bitmap clipboard format (CF_BITMAP
).
public class myDataObject : IDataObject
{
private Bitmap mBitmap;
public FORMATETC mpFormatetc;
#region IDataObject Members
private const uint S_OK = 0;
private const uint E_POINTER = 0x80004003;
private const uint E_NOTIMPL = 0x80004001;
private const uint E_FAIL = 0x80004005;
public uint GetData(ref FORMATETC pFormatetc,
ref STGMEDIUM pMedium)
{
IntPtr hDst = mBitmap.GetHbitmap();
pMedium.tymed = (int)TYMED.TYMED_GDI;
pMedium.unionmember = hDst;
pMedium.pUnkForRelease = IntPtr.Zero;
return (uint)S_OK;
}
...
#endregion
public myDataObject()
{
mBitmap = new Bitmap(16, 16);
mpFormatetc = new FORMATETC();
}
public void SetImage(string strFilename)
{
try
{
mBitmap = (Bitmap)Bitmap.FromFile(strFilename, true);
mpFormatetc.cfFormat = CLIPFORMAT.CF_BITMAP;
mpFormatetc.ptd = IntPtr.Zero;
mpFormatetc.dwAspect = DVASPECT.DVASPECT_CONTENT;
mpFormatetc.lindex = -1;
mpFormatetc.tymed = TYMED.TYMED_GDI;
}
catch
{
}
}
public void SetImage(Image image)
{
try
{
mBitmap = new Bitmap(image);
mpFormatetc.cfFormat = CLIPFORMAT.CF_BITMAP;
mpFormatetc.ptd = IntPtr.Zero;
mpFormatetc.dwAspect = DVASPECT.DVASPECT_CONTENT;
mpFormatetc.lindex = -1;
mpFormatetc.tymed = TYMED.TYMED_GDI;
}
catch
{
}
}
}
Take a look at how the member method SetImage
creates a Bitmap
object to use its handle when the GetData
is called.
Now, here is how the object is inserted into the RichEditBox
creating a shared global memory and getting a pointer to it (IStorage
) and using the OleClientSite
interface from the IRichEditOle
.
public void InsertMyDataObject(myDataObject mdo)
{
if (mdo == null)
return;
ILockBytes pLockBytes;
int sc = CreateILockBytesOnHGlobal(IntPtr.Zero,
true, out pLockBytes);
IStorage pStorage;
sc = StgCreateDocfileOnILockBytes(pLockBytes, (uint)
(STGM.STGM_SHARE_EXCLUSIVE|STGM.STGM_CREATE|
STGM.STGM_READWRITE),
0, out pStorage);
IOleClientSite pOleClientSite;
this.IRichEditOle.GetClientSite(out pOleClientSite);
Guid guid = Marshal.GenerateGuidForType(mdo.GetType());
Guid IID_IOleObject =
new Guid("{00000112-0000-0000-C000-000000000046}");
Guid IID_IDataObject =
new Guid("{0000010e-0000-0000-C000-000000000046}");
Guid IID_IUnknown =
new Guid("{00000000-0000-0000-C000-000000000046}");
object pOleObject;
int hr = OleCreateStaticFromData(mdo, ref IID_IOleObject,
(uint)OLERENDER.OLERENDER_FORMAT, ref mdo.mpFormatetc,
pOleClientSite, pStorage, out pOleObject);
if (pOleObject == null)
return;
OleSetContainedObject(pOleObject, true);
REOBJECT reoObject = new REOBJECT();
reoObject.cp = this._richEdit.TextLength;
reoObject.clsid = guid;
reoObject.pstg = pStorage;
reoObject.poleobj = Marshal.GetIUnknownForObject(pOleObject);
reoObject.polesite = pOleClientSite;
reoObject.dvAspect = (uint)(DVASPECT.DVASPECT_CONTENT);
reoObject.dwFlags = (uint)(REOOBJECTFLAGS.REO_BELOWBASELINE);
reoObject.dwUser = 0;
this.IRichEditOle.InsertObject(reoObject);
Marshal.ReleaseComObject(pLockBytes);
Marshal.ReleaseComObject(pOleClientSite);
Marshal.ReleaseComObject(pStorage);
Marshal.ReleaseComObject(pOleObject);
}
There are other methods to insert controls and ActiveX objects, they look very similar to the above method, so please review the source code.
Points of Interest
And finally, how are the controls updated?
This is the trick, you need to use a timer and call the method UpdateObjects
. This method performs a search for all objects in the RichTextBox
and if they are marked as special (in my case I use the dwUser
variable), they will be updated:
public void UpdateObjects()
{
int k = this.IRichEditOle.GetObjectCount();
for (int i = 0; i < k; i++)
{
REOBJECT reoObject = new REOBJECT();
this.IRichEditOle.GetObject(i, reoObject,
GETOBJECTOPTIONS.REO_GETOBJ_ALL_INTERFACES);
if (reoObject.dwUser == 1)
{
Point pt = this._richEdit.GetPositionFromCharIndex(reoObject.cp);
Rectangle rect = new Rectangle(pt, reoObject.sizel);
this._richEdit.Invalidate(rect, false);
}
}
}
There is a lot of work required for optimizing this control but for now, any suggestion is appreciated.
Using the code
To use the code, simply add a reference to the control, put a normal RichTextBox
into the form and then replace the type for MyExtRichTextBox
:
MyExtRichTextBox.MyExtRichTextBox richTextBox1;
Tip
I update objects by creating an array of controls (buttons and progress bars) and adding a timer to the form, then calling the method UpdateObjects
like this:
private void timer1_Tick(object sender, System.EventArgs e)
{
for (int i = 0; i < ar.Count; i++)
{
itimer++;
if (itimer > 100)
itimer = 0;
object obj = ar[i];
if (obj is Button)
{
Button bt = (Button) obj;
if (bt.Text != "Clicked")
bt.Text = "button " + i.ToString() +
" - " + itimer.ToString();
}
else
{
ProgressBar pb = (ProgressBar) obj;
if (pb.Value + 1 > 100)
pb.Value = 0;
pb.Value = pb.Value + 1;
}
}
richTextBox1.UpdateObjects();
}
History
- Version 1.0 - Nov. 1 / 2005