ActiveX controls have been the mainstay of most Visual Basic
6 developers and have found their way into countless development
tools and Windows applications. Most ActiveX controls, even the
"free" controls included with Visual Basic, can be used with Visual
C++, Access, Delphi, Oracle, Paradox, and many other languages.
With tens of thousands of ActiveX controls available, some
free, some not, it's not difficult to find a control with the exact
feature that you need for your latest project. Finding a "pure" .NET
managed control is not always quite that simple.
.NET and Visual Studio .NET go a long way toward making
ActiveX controls very easy to use, but there are some pitfalls that
may cause some confusion (and possibly some bad language). These
obstacles are really minor, but when you run into them the first
time, they are extremely frustrating.
I'll try to guide you through most of these issues. No, we
won't write an actual application, but once you have seen the
problems you may encounter, you should be ready to go with the
assurance that you can use most ActiveX controls with .NET. To
illustrate the roadblocks ahead, I'll use the demo version of
SftBox/ATL 3.0, an ActiveX ComboBox control. You can download the
free demo from www.windowscontrols.com, but most any ActiveX control
will do and could be used instead.
I'll use Visual Studio .NET 2003 in this article and for the
sample code, but using Visual Studio .NET 2002 is essentially the
same and raises the same issues when dealing with an ActiveX control.
Both versions fully support ActiveX controls, and the techniques
shown here apply to both.
Getting ready to use an ActiveX control and dropping it onto
a Windows Form is quite straightforward. First, you'll need to create
a new project. Using Visual Studio .NET's "File, New" menu command,
create a new Windows Application using the Visual C# or Visual Basic
Project type. As the blank form stares at you, it is time to drop an
ActiveX control onto it. Your Toolbox has a number of Windows Forms
controls, but you'll have to add the ActiveX controls you want to use
first. If you right-click on the Toolbox and select Add/Remove Items
(or Customize Toolbox in Visual Studio .NET 2002), you'll be
presented with a list of .NET components and a list of COM
components. ActiveX controls are COM components, so you'll have to
pick one of the COM components. In this example, we'll select
"SftBox/ATL 3.0 Combo Box Control" (see Figure 1).
Now you're all set. Locate the new Toolbox icon for the
SftBox/ATL 3.0 Combo Box Control and drop it on your form (see Figure
2).
So far, so good. If your ActiveX control made it this far,
we'll assume that it works okay with .NET. Just keep in mind that
some ActiveX controls simply won't work with all development tools.
For example, most tab controls that you may be familiar with from
your Visual Basic 6 days do not work with anything but Visual Basic
6; some splitter controls also suffer from this limitation. But for
the most part, a well-designed control that follows the ActiveX
standards should offer little trouble.
At this point, you'll most likely need some help. Actually,
most of us would. Unless you know the control you are using extremely
well, you'll probably need a little guidance in the form of online
help. Not all of us manage to remember the hundreds of properties and
methods a control offers. Finding the help file is not always easy.
Older ActiveX controls or controls that have not been updated
specifically to support .NET will not offer online help - at least
it's not easily accessible. Many current ActiveX controls do,
however, have excellent help support. When a control is selected or a
property is edited, the Dynamic Help window automatically offers a
number of suggested topics for the control and for Visual Studio .NET
in general. Dynamic Help is a great way to access context-related
help for any activity. You can access Dynamic Help using Control F1.
For controls that don't offer Dynamic Help, you'll have to locate the
help file. Often you'll find a program group for a control in your
Start menu, which has an entry for online help. If the control offers
property pages, you may be able to access online help there.
Otherwise, you'll have to hunt for the help file.
So far I've shown you how to add a control to the form and
access online help. I skipped over a little detail when I told you to
drop the control onto the form. Remember, this is an ActiveX control.
.NET doesn't really "talk" directly to ActiveX controls. An ActiveX
control is not a managed control; it uses unmanaged code. Visual
Studio .NET creates a complete class wrapper for our ActiveX control,
so to your application the control looks just like any other .NET
managed control would. This class wrapper is called the runtime
callable wrapper (RCW). Usually, this process is completely
transparent and is a nice way to bring an ActiveX control to the .NET
world. An ActiveX control will now look just like a .NET control -
and in fact it is. The class wrapper creates a control derived from
the AxHost class, which has the Control class as its base class.
Where is the class wrapper? You don't actually see it. It is
created, compiled, and deleted. You get only the Interop DLLs in your
output directory. But for the curious (and sometimes the desperate),
you can take a look at the class wrapper. The .NET Framework SDK has
a utility that can generate the source (in addition to creating the
DLLs). The AxImp utility can create the class wrapper for you. Its
output is also included as part of the sample source available with
this article. Use this command- line code to invoke the utility:
"C:\program files\Microsoft Visual Studio .NET
2003\SDK\v1.1\Bin\AxImp.exe"
C:\Windows\System32\SftBox_IX86_A_30.ocx/source
It creates a source file full of quite boring properties and
method definitions, as seen in Listing 1. For now, we won't actually
need this class wrapper, but it will come in handy when we look at
events and a few odd properties.
Up to this point an ActiveX control and a .NET managed
control are virtually indistinguishable. They certainly look the same
when your application runs. Even at design time, you can manipulate
properties using the IDE's "Properties" window and you can get online
help. Some ActiveX controls even offer property pages. You can access
these using the Property Pages button in the Properties window. Or
you can access the Properties pages by right-clicking on the control
in design-view. Properties pages offered by ActiveX controls are
specific to each control (see Figure 3) and often offer additional
features, such as easier property editing, online help access, and
additional descriptions.
Whether you modify properties through the IDE's Properties
window or through the control's custom property pages doesn't matter.
All the properties you define at design time are nicely saved in a
resource and are set at runtime by the form's InitializeComponent
method (see Listing 2).
It looks all too easy, and as usual in this business, it's
the little details that get you. Let's start by adding three entries
to the combo box and selecting the first entry (see Listing 3). As
you enter this code, you'll notice that IntelliSense nicely offers
all available properties and methods. The project actually runs
without any problems, but it does look quite boring.
First, let's spruce it up a bit by using a different
foreground color for the first entry. In old-style VB, we would be
able to say something like:
SftBox1.Cell(index, 0).ForeColor = vbRed;
Basically, we're accessing the Cell object, indexed by item
index and column index, so we can manipulate its ForeColor property.
Because of the class wrapper generated by .NET, it's not quite that
simple. This seemingly simple construct can make a seasoned developer
cringe when it needs to be translated to .NET. But you'll get the
hang of it once you know what to look for. Simple properties are no
problem. But a property such as "Cell", which requires additional
parameters (i.e., index and column number), is changed by .NET into a
method. So instead of the Cell property, you'll have to use the
get_Cell and set_Cell methods. Actually set_Cell doesn't exist in
this case, but you get the idea. So let's try again.
C#
axSftBox1.get_Cell(index, 0).ForeColor = Color.Red;
VB
AxSftBox1.get_Cell(index, 0).ForeColor = Color.Red
Almost, but IntelliSense says ForeColor is of type uint or
UInt32, and Red is a Color type. Fortunately, .NET has a
ColorTranslator class, which comes in handy to convert between these
types:
C#
axSftBox1.get_Cell(index, 0).ForeColor = (uint)
ColorTranslator.ToOle(Color.Red);
axSftBox1.get_Cell(index, 0).SelectForeColor =
axSftBox1.get_Cell(index, 0).ForeColor
VB
AxSftBox1.get_Cell(Index, 0).ForeColor =
Convert.ToUInt32(ColorTranslator.ToOle(Color.Red))
AxSftBox1.get_Cell(Index, 0).SelectForeColor =
AxSftBox1.get_Cell(Index, 0).ForeColor
Now let's add a nice little image to the first item. To make
life easier, we'll add a PictureBox control from the Toolbox to the
form, load an image into it, and we'll have access to the Image
property of the PictureBox. Of course there are many other ways to
get an image, but this is the easiest for our example. .NET thinks of
bitmaps and images in terms of an Image object. ActiveX controls use
the OLE Picture object (really an IPicture or IPictureDisp COM
interface). These are truly worlds apart. In "VB6-speak" you would
simply assign the image to the cell as in:
Set SftBox1.Cell(index, 0).Picture = PictureBox1.Picture;
A VB6 PictureBox happens to have the Picture property, which
uses an OLE Picture object. .NET of course doesn't. Converting an
Image object to an IPicture interface pointer seems difficult, if not
impossible. At first, this is a real puzzler. A look at the class
wrapper may help. It translated the MouseIcon property, which also
uses an OLE Picture type, but it is exposed it as a
System.Drawing.Image type. Upon inspection, it reveals the use of the
GetIPictureFromPicture method.
this.ocx.MouseIcon = ((stdole.IPictureDisp)(GetIPicture
FromPicture(value)));
This method takes an object of type Image and returns an
IPicture interface pointer. That's exactly what we need, but
unfortunately the method is protected, meaning it can only be used in
a class derived from AxHost. So, we'll have to create a small helper
class for this. Fortunately, we'll only have to do this once. All
controls and projects can use the same helper class. The
OLEConvert.cs and OLEConvert.vb source files included with the source
code for this article show how this class is implemented (see Listing
4).
The ToOLEPic function is used to easily convert from an Image
object to an OLE Picture type.
C#
axSftBox1.get_Cell(index, 0).Picture = (stdole.IPictureDisp)
OLEConvert.OLECvt.ToOLEPic(pictureBox1.Image);
VB
p>
AxSftBox1.get_Cell(Index, 0).Picture =
OLEConvert.OLECvt.ToOLEPic(PictureBox1.Image)
The complete code to add three entries, highlighting the
first with color and an image, now looks as shown in Listing 5.
While entering code, you may have noticed that IntelliSense
also offers a let_Picture method. Usually, a picture property offers
two forms. The first form assigns a reference to a picture object
(Picture property), the second form assigns a complete copy of the
image to the picture property (let_Picture method). Unless you want
to create actual copies of images, the use of let_Picture should be
minimal.
Fonts also need somewhat special treatment. .NET knows the
Font type, but ActiveX controls insist on the OLE Font type (a
IFontDisp COM interface). The generated class wrapper again offers
some assistance and handles the control's Font property, which
accepts a Font type and translates it to the ActiveX control's
preferred IFontDisp interface pointer, so we don't have to worry
about this case (see Listing 6).
Unfortunately, other Font properties, such as the cells' Font
properties, need our help. Our helper class that we implemented to
better handle images can also help with fonts. The ToOLEFont function
converts a Font object to an IFontDisp interface pointer (see Listing
7).
Dealing with images and fonts can be difficult at first, but
using these techniques should make it quite straightforward. Of
course, you'll still have to write the application, but at least
working with ActiveX controls is now a bit easier.
Because .NET uses namespaces, even setting a simple property
can end up becoming a typing effort.
axSftBox1.Items.Style =
SftBoxLib30.SftBoxItemsStyleConstants.itemsStyleSftBoxVariable;
Don't forget that you can use a using or Imports statement
even for ActiveX controls. This will reduce the clutter. If you add a
"using SftBoxLib30;" or an "Imports SftBoxLib30" statement to your
source file, assigning a property becomes:
axSftBox1.Items.Style = SftBoxItemsStyleConstants.itemsStyleSftBoxVariable;
Events are a bit surprising in the .NET world. If you are
used to Visual Basic or Visual C++, you expect event parameters to
arrive as part of the event handler. For example, in Visual Basic 6
you would see a KeyDown event defined like this:
Private Sub object_KeyDown(KeyCode As Integer, ByVal Shift As Integer)
In .NET, every event handler has the same "signature" with
exactly two arguments:
C#
void object_KeyDownEvent(object sender, EventArgumentType e);
VB
Private Sub AxSftBox1_KeyDownEvent(ByVal sender As Object, ByVal e As
AxSftBoxLib30._ISftBoxEvents_KeyDownEvent) Handles
AxSftBox1.KeyDownEvent
All the arguments that are specific to the event are passed
in the "e" object. "e" is an object of class EventArgs or a derived
class. You'll find all the different event classes listed in the
class wrapper generated by AxImp. But generally, you really won't
have to look there. If you need a specific argument, you can use
IntelliSense by typing "e." and it will offer all available
arguments. You should have no problem matching these up against the
documented arguments in the online help. The generated class wrapper
usually changes the argument's first letter to lowercase. In this
example you would find an e.keyCode and an e.shift member as part of
the class derived from EventArgs (see Listing 8).
At last, depending on the ActiveX control you are using, you
may find that a method, property, or event is missing or may be in
conflict with one of its base classes. (Remember, your ActiveX
control is derived from the Control and AxHost base classes.) For
example, the ComboBox control's "RightToLeft" property is nowhere to
be found. With the generated class wrapper in hand, you can quickly
find that the property has become "CtlRightToLeft" instead. .NET
prefixes conflicting or reserved names with "Ctl". If a control has a
"Refresh" method, it is renamed to "CtlRefresh", to distinguish it
from the Refresh method defined in the Control base class. Some
events receive an "Event" suffix. For example, an event name
"KeyDown" (as defined by the ActiveX control) becomes the
"KeyDownEvent".
Conclusion
Using ActiveX controls is just as easy with .NET as it is
with many other development tools such as Visual Basic. Most ActiveX
controls will work equally well in a .NET application. The .NET
runtime offers great COM support, so you can continue to use ActiveX
controls that you have used outside of .NET until now. Rather than
reinventing the wheel or waiting for native .NET controls to catch up
with the selection and the wealth of features already offered by
ActiveX controls, you can use these controls today. Now that you know
how to overcome some of the more frustrating issues involved in
integrating ActiveX controls with .NET, what is stopping you?
Author Bio
Frank Sloane is support manager at Softel vdm, a company specializing
in Windows ActiveX and .NET controls. He has coauthored (and fixed) a
number of applications and controls since joining the company in 1997.
Listing 1
[System.ComponentModel.Browsable(false)]
[System.ComponentModel.DesignerSerializationVisibility
(System.ComponentModel.DesignerSerializationVisibility.Hidden)]
[System.Runtime.InteropServices.DispIdAttribute(9)]
public virtual SftBoxLib30.SftBoxEdit Edit {
get {
if ((this.ocx == null)) {
throw new System.Windows.Forms
.AxHost.InvalidActiveX
StateException("Edit",
System.Windows.Forms.AxHost.
ActiveXInvokeKind.PropertyGet);
}
return this.ocx.Edit;
}
}
Listing 2
C#
private void InitializeComponent()
{
System.Resources.ResourceManager resources = new
System.Resources.ResourceManager(typeof(Form1));
this.axSftBox1 = new AxSftBoxLib30.AxSftBox();
((System.ComponentModel.ISupportInitialize)(this.axSftBox1)).BeginInit();
this.SuspendLayout();
//
// axSftBox1
//
this.axSftBox1.Location = new System.Drawing.Point(32, 24);
this.axSftBox1.Name = "axSftBox1";
this.axSftBox1.OcxState = ((System.Windows.Forms.
AxHost.State)(resources.GetObject
("axSftBox1.OcxState")));
this.axSftBox1.Size = new System.Drawing.Size(224, 38);
this.axSftBox1.TabIndex = 0;
//
// Form1
//
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
this.ClientSize = new System.Drawing.Size(292, 266);
this.Controls.Add(this.axSftBox1);
this.Name = "Form1";
this.Text = "Form1";
this.Load += new System.EventHandler(this.Form1_Load);
((System.ComponentModel.ISupportInitialize)(this.axSftBox1)).EndInit();
this.ResumeLayout(false);
}
VB
Private Sub InitializeComponent()
Dim resources As System.Resources.ResourceManager = New
System.Resources.ResourceManager
(GetType(Form1))
Me.AxSftBox1 = New AxSftBoxLib30.AxSftBox
CType(Me.AxSftBox1,
System.ComponentModel.ISupportInitialize)
.BeginInit()
Me.SuspendLayout()
'
'AxSftBox1
'
Me.AxSftBox1.Location = New System.Drawing.Point(32, 24)
Me.AxSftBox1.Name = "AxSftBox1"
Me.AxSftBox1.OcxState = CType(resources.GetObject
("AxSftBox1.OcxState"),
System.Windows.Forms.AxHost.State)
Me.AxSftBox1.Size = New System.Drawing.Size(224, 38)
Me.AxSftBox1.TabIndex = 3
'
'Form1
'
Me.AutoScaleBaseSize = New System.Drawing.Size(5, 13)
Me.ClientSize = New System.Drawing.Size(292, 266)
Me.Controls.Add(Me.AxSftBox1)
Me.Name = "Form1"
Me.Text = "Form1"
CType(Me.AxSftBox1,
System.ComponentModel.ISupportInitialize)
.EndInit()
Me.ResumeLayout(False)
End Sub
Listing 3
C#
private void Form1_Load(object sender, System.EventArgs e) {
axSftBox1.Items.Style =
SftBoxLib30.SftBoxItemsStyleConstants.
itemsStyleSftBoxVariable;
int index = axSftBox1.Items.Add("First Item");
axSftBox1.Items.Add("Second Item");
axSftBox1.Items.Add("Third Item");
axSftBox1.Items.Selection = index;
}
VB
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs)
Handles MyBase.Load
Dim Index As Integer
AxSftBox1.Items.Style =
SftBoxLib30.SftBoxItemsStyleConstants.itemsStyleSftBoxVariable
Index = AxSftBox1.Items.Add("First Item")
AxSftBox1.Items.Add("Second Item")
AxSftBox1.Items.Add("Third Item")
AxSftBox1.Items.Selection = Index
End Sub
Listing 4
C#
namespace OLEConvert
{
// This helper class allows access to pro
// tected members of AxHost
// used to convert Image and Font objects to
// OLE objects
public class OLECvt : System.Windows.Forms.AxHost
{
private OLECvt() : base("")
{
}
// convert an Image to an OLE Picture object
public static stdole.StdPictureToOLEPic(Image i)
{
return
AxHost.GetIPictureDispFromPicture(i) as stdole.StdPicture;
}
// convert an Image to an OLE PictureIPictureDisp interface
public static stdole.IPictureDisp
ToOLE_IPictureDisp(Image i)
{
return
AxHost.GetIPictureDispFromPicture(i) as stdole.IPictureDisp;
}
// convert a Font to an OLE Font object
public static stdole.StdFontToOLEFont(Font f)
{
return AxHost.GetIFontFromFont(f) as stdole.StdFont;
}
// convert a Font to an OLE Font
// IFontDisp interface
public static stdole.IFontDispToOLE_IFontDisp(Font f)
{
return AxHost.GetIFontFromFont(f) as stdole.IFontDisp;
}
}
}
VB
Public Class OLECvt
Inherits System.Windows.Forms.AxHost
Public Sub New()
MyBase.New("")
End Sub
' convert an Image to a OLE Picture object
Public Shared Function ToOLEPic(ByVal i As Image) As stdole.StdPicture
Return GetIPictureDispFromPicture(i)
End Function
' convert an Image to an OLE Picture ' IPictureDisp interface
Public Shared Function ToOLE_IPictureDisp(ByVal i As Image) As
stdole.IPictureDisp
Return GetIPictureDispFromPicture(i)
End Function
' convert a Font to an OLE Font object
Public Shared Function ToOLEFont(ByVal f As Font) As stdole.StdFont
Return GetIFontFromFont(f)
End Function
' convert a Font to an OLE Font IFontDisp ' interface
Public Shared Function ToOLE_IFontDisp(ByVal f As Font) As stdole.IFontDisp
Return GetIFontFromFont(f)
End Function
End Class