Introduction
I've been out of programming for quite a while and I'm catching up on the technology I've missed since I left. These articles and controls are an attempt to learn how to develop controls that are easy to use and look good, and pass this knowledge on to others that might be sharing this same path. From the response I've gotten on my two previous articles, it seems I might be on the right path. Thank you all for the encouragement and all the good people here on CodeProject.
I would also like to give credit to James T. Johnson for his excellent article on CodeProject "Getting to know IExtenderProvider" for the concept.
This is the second article in the Owner Drawn series. In this instalment, we'll be looking at the MenuItem
component and extending it to suit our needs. I had originally written an app a few years ago using an earlier version of this component but had to configure MenuItem
manually so when I decided to update and enhance it, I ran across the article mentioned above and thought "so that's how you do that". What the extender does is allows us to add design time properties to allMenuItem
components. Similar to ToolTips!
Disclaimer: For this demo, I've provided a very limited version of this component for instructional purposes only. The code can be used as a template to extend it to meet your own specs. I only ask that if you use it, you give credit to me and if possible let me know what you did with it. I'd appreciate it!
Concepts
When creating an Owner Drawn control/component, there are three areas that need to be addressed that will allow us to customize the control. As a general rule, we derive a control and override the following event handlers to handle this interaction.
public class MyListBox : ListBox
{
protected override void OnMeasureItem(System.Windows.Forms.MeasureItemEventArgs e)
{
base.OnMeasureItem(e);
}
protected override void OnDrawItem(System.Windows.Forms.DrawItemEventArgs e)
{
base.OnDrawItem(e);
}
protected override void OnMouseDown(System.Windows.Forms.MouseEventArgs e)
{
base.OnMouseDown(e);
}
}
and set the controls DrawMode
property to OwnerDraw
in the designer or programmatically:
MyListBox.DrawMode = DrawMode.OwnerDrawVariable;
In the following sections, I will describe the design methodology needed to implement the functionality provided in this demo. I'm taking a layered approach in the way in which I will present this material starting with an overview of general concepts and working down to specifics, using code snippets and examples. Kinda like a well digger... he starts digging with a shovel at the surface and kinda corkscrews his way into the ground until he either gets worn out or finds water.
Break Out the Shovel
The rule above applies to this component but we have to be a tad creative here. We use the IExtenderProvider
interface to allow our custom properties to be applied to all MenuItem
components to be added to the MainMenu
or ContextMenu
components. This is a small step back from the MenuStrip
and ContextMenuStrip
components supplied with VS2005 but this step is required if you want to deviate from the supplied implementation. We do this by creating a component derived from component and implementing the IExtenderProvider
interface, declare the properties we are providing to the component, then install and supply handlers for the requisite events. If we have done this correctly, the new extender can be dug from the toolbox onto the component tray and we will see our properties in the PropertyGrid
for the MenuItems
that we add to the MainMenu
or ContextMenu
components.
Digging In
OK, enough talk. Let's roll up our sleeves and sweat out some of that beer we drank last night.
First we need to declare the attributes we intend to associate with the component:
[ProvideProperty("XPMenuItem", typeof(MenuItem))]
[ProvideProperty("XPMenuItemImage", typeof(MenuItem))]
[ProvideProperty("XPHeaderText", typeof(MenuItem))]
[ProvideProperty("XPHeaderImage", typeof(MenuItem))]
class XMenuItemExtender : Component, IExtenderProvider
{
Notice that each attribute declared is to be associated with the MenuItem
component, but as we will see later, the property itself can be of any type.
Because there may be many instances of MenuItem
associated with this extender, we will use a hash table to store references to the objects, along with the associated properties for each item. We will further use the item itself as the key into the hash table to reference properties for that item.
The regular property Get
/Se
t
definitions cannot be used here. We must replace them with their counterpart methods as shown here:
public string GetXPHeaderText(MenuItem mi)
{
return EnsurePropertyExists(mi)._headerText;
}
public void SetXPHeaderText(MenuItem mi, string str)
{
EnsurePropertyExists(mi)._headerText = str;
}
The coding is typical for all properties. The EnsurePropertyExists
is a method that guarantees that we will always have a reference to a valid object. For a complete description of IExtenderProvider
, see article mentioned in the Introduction.
In Deep Water
Now let's get the event handlers hooked up and start using this new toy.
public void SetXPMenuItem(MenuItem mis, MenuItem mid)
{
EnsurePropertyExists(mis)._menuItem = mid;
if (mid != null)
{
mis.MeasureItem += new MeasureItemEventHandler(OnMeasureItem);
mis.DrawItem += new DrawItemEventHandler(OnDrawItem);
mis.Click += new EventHandler(OnClick);
}
else
{
mis.MeasureItem -= new MeasureItemEventHandler(OnMeasureItem);
mis.DrawItem -= new DrawItemEventHandler(OnDrawItem);
mis.Click -= new EventHandler(OnClick);
}
}
Each time an item is added to the extender, it installs the event handlers we need along with the item. All we need to do now is to implement the handlers for each and we're ready to rock-n-roll.
private void OnClick(object sender, EventArgs e)
private void OnMeasureItem(object sender, MeasureItemEventArgs e)
private void OnDrawItem(object sender, DrawItemEventArgs e)
NOTE: I've used the override names for consistency, they may be any legal name.
The way I've laid out the MenuItem
itself is a little restrictive. I divide the item into header and client areas and assume that header and items are a fixed size. The header information is retained in the first item in each menu along with the information for that item. I use the following calculation to determine the overall height of the menu;
Rectangle hrct = new Rectangle(0, 0, 20, mi.Parent.MenuItems.Count * 20);
Where mi.Parent.MenuItems.Count
is the number of items in the Menu
. To have a variable size item, you will need to add each individual items height to a total to determine true height of Menu
. When I go to draw the items, I use the first item in the menu as a trigger to draw the header area, then I don't touch that area on subsequent draws. I treat it like a nonclient area.
I've tried to comment the code to make it easier to follow and so I would only have to touch on key points in the article itself. Although theory is important, I always like to dig in first and anything I don't understand, I try to find the explanation for in the article.
Points of Interest
Ever wonder where the expression "Colder than a well diggers a**" came from? Well, after you get a few feet down, the ground starts to get cold and the diggers posterior rests against the walls all the way down!
History