Introduction
I started this project when I needed something like the toolbar in FireFox's Options dialog in one of my other projects. This is where the name came from. I looked around but couldn't find any control that had what I needed so I decided to write my own.
Another benefit of writing this control was that it gave me the opportunity to play around with some of the techniques used in control creation, i.e., Designers, TypeConverter
s and TypeEditor
s. I will not go into code details (that's what the source code is for) but I will try to mention a little about the techniques used.
I took a lot of hints from Matthew Hall's Themed Taskbar for general design and use of collections, and I also borrowed John O' Byrne's excellent Imagelistpopup control, modified it slightly and used it in my image dropdown editor.
Using the control
Like any other .NET control, for use in the IDE, you should add the MozBar
control to a Toolbox panel. This can be accomplished by right-clicking on a Toolbox tab and selecting "Add/Remove items...", browsing to the MozBar
assembly, and selecting it. This will add all the MozBar
controls to the Toolbox so they can be dragged/dropped to a Windows Form or control.
There are two primary controls in the MozBar
assembly:
A MozItem
is the control that defines a toolbar item. A MozItem
must be contained by a MozPane
. A MozPane
is a container for a collection of MozItem
controls, and provides automatic placement and relocation as they are added or removed.
Once an instance of MozPane
or MozItem
is added to the form, select it and view its properties.
MozPane
The MozPane
acts as a container for all the MozItem
s that the MozBar
will contain.
There are several properties that define the appearance and behavior of the MozBar
:
Style
: Sets the orientation/layout of the MozBar
, can be set to Vertical
or Horizontal
.
ImageList
: ImageList
that contains the images used by the items. If this is not set, no images can be selected for the items.
ItemColors
: The colors used for an item's various states.
ItemBorderStyles
: The border styles used for an item's various states.
Padding
: Property that specifies the spacing between items and border, both vertically and horizontally.
Items
: Collection containing the items within the MozBar
.
Toggle
: Determines if it should be possible to toggle the selected state of the items. If this is set to false
the only way to deselect an item is to select another.
SelectedItems
: Returns the number of selected items.
MaxSelectedItems
: The max. number of simultaneous selected items, this only has effect if Toggle
is set to true
. If Toggle
is false
, MaxSelectedItems
is always 1.
SelectButton
: The mouse button used for selections.
Theme
: Indicates whether the control should use theme colors. If enabled and visual themes are not supported, system colors will be used.
The most useful events are:
ItemClick
: Indicates an item has been clicked.
ItemDoubleClick
: Indicates an item has been double clicked.
ItemGotFocus
: Indicates that an item has got focus.
ItemLostFocus
: Indicates that an item has lost focus.
ItemSelected
: Indicates that an item has been selected.
ItemDeselected
: Indicates that an item has been deselected, i.e., toggled. This event can only be raised if Toggle
is true
.
All eventArgs
s include the MozItem
responsible for the event. To check which item caused the event, use the Tag
property.
private void mozPane1_ItemSelected(object sender, Pabo.MozBar.MozItemEventArgs e)
{
switch(e.MozItem.Tag)
{
case "Save":
{
break;
}
case "Load":
{
break;
}
}
}
The ItemClick
event uses the ItemClickEventArgs
which in addition to the responsible MozItem
also includes the Button
property which contains the button used.
To select an item, use the methods SelectItem(int index)
or SelectItem(string tag)
, you have the option of using either the index or the tag to identify the item.
MozItem
A MozItem
is the actual toolbar item and it can be added to MozPane
by simply dragging it onto the MozPane
from the toolbox or by using the Controls
property in the MozPane
control.
A MozItem
can have one of three possible states:
Each of the states can have a different image, borderstyle and color for background and border. The images are set through the item's Images
property; colors and borderstyles are set with the ItemColors
and ItemBorderStyles
properties in the MozPane
control.
An item can also have different styles, that are set using the ItemStyle
property:
Picture
TextAndPicture
Text
Divider
When set to Divider
, the item will show as a divider (similar to the ones used in menus) either horizontally or vertically depending on the Style
of the MozPane
. If TextAndPicture
is used, the Text
can be aligned using the TextAlign
property, possible positions are Left
, Top
, Right
or Bottom
.
Nested properties
A nice way to organize related properties in a control is to group them together in a nested structure, common examples of this in controls are Size
and Font
properties. In MozBar
, this technique is used with several properties, among them the Padding
property in MozPane
.
To accomplish this, we need to do two things. First, we need a class (PaddingCollection
) that contains the properties we want grouped together, and second, we need a TypeConverter
(PaddingCollectionTypeConverter
) to display the properties properly.
[TypeConverter(typeof(PaddingCollectionTypeConverter))]
public class PaddingCollection
{
private MozPane m_pane;
private int m_horizontal;
private int m_vertical;
public PaddingCollection(MozPane pane)
{
m_pane = pane;
m_horizontal = 2;
m_vertical = 2;
}
[RefreshProperties(System.ComponentModel.RefreshProperties.All)]
[Description("Horizontal padding.")]
public int Horizontal
{
get
{
return m_horizontal;
}
set
{
m_horizontal = value;
if (m_pane!=null)
{
m_pane.DoLayout();
m_pane.Invalidate();
if (m_pane.PaddingChanged!=null)
m_pane.PaddingChanged(this,new EventArgs());
}
}
}
[RefreshProperties(System.ComponentModel.RefreshProperties.All)]
[Description("Vertical padding.")]
public int Vertical
{
get
{
return m_vertical;
}
set
{
m_vertical = value;
if (m_pane!=null)
{
m_pane.DoLayout();
m_pane.Invalidate();
if (m_pane.PaddingChanged!=null)
m_pane.PaddingChanged(this,new EventArgs());
}
}
}
}
The class is pretty straightforward but the important things are the attributes. The TypeConverter
attribute is used to assign the type converter to the class.
[TypeConverter(typeof(PaddingCollectionTypeConverter))]
public class PaddingCollection
{
}
The RefreshProperties
attribute is used to force a refresh of properties when the property to which the attribute is assigned is changed.
[RefreshProperties(System.ComponentModel.RefreshProperties.All)]
public int Horizontal
{
}
Since this is a nested property, the type converter should inherit from ExpandableObjectConverter
and we need to override four methods. In CanConvertTo
and CanConvertFrom
, we need to make sure we can convert to and from strings. The actual conversion is done in the ConvertTo
and ConvertFrom
functions.
public class PaddingCollectionTypeConverter : ExpandableObjectConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context,
Type sourceType)
{
if(sourceType == typeof(string))
return true;
return base.CanConvertFrom (context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext context,
Type destinationType)
{
if(destinationType == typeof(string))
return true;
return base.CanConvertTo (context, destinationType);
}
public override object ConvertFrom(ITypeDescriptorContext
context, System.Globalization.CultureInfo culture, object value)
{
if(value.GetType() == typeof(string))
{
string[] ss = value.ToString().Split(new char[] {';'}, 2);
if (ss.Length==2)
{
PaddingCollection item =
new PaddingCollection((MozPane)context.Instance);
item.Horizontal = int.Parse(ss[0]);
item.Vertical = int.Parse(ss[1]);
return item;
}
}
return base.ConvertFrom (context, culture, value);
}
public override object ConvertTo(ITypeDescriptorContext context,
System.Globalization.CultureInfo culture,
object value, Type destinationType)
{
if(destinationType == typeof(string) &&
(value is MozPane.PaddingCollection) )
{
PaddingCollection dest = (PaddingCollection)value;
return dest.Horizontal.ToString()+"; "+dest.Vertical.ToString();
}
return base.ConvertTo (context, culture, value, destinationType);
}
}
All that's left to do is to create a public
property in MozPane
that handles a value of type PaddingCollection
, and we are all set.
true)>
[Category("Appearance")]
[Description("Padding (Horizontal, Vertical)")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public PaddingCollection Padding
{
get
{
return m_padding;
}
set
{
if (value!=m_padding)
{
if (value != null)
m_padding = value;
DoLayout();
Invalidate();
if (this.PaddingChanged!=null)
this.PaddingChanged(this,new EventArgs());
}
}
}
Custom image selector
Another nice thing to provide with controls is custom type editors. Each of the image states in MozItem
uses a custom image selector for selecting images from the ImageList
assigned in the MozPane
.
For this to work, we must create our own type editor (ImageMapEditor
) that inherits from System.Drawing.Design.UITypeEditor
.
public class ImageMapEditor : System.Drawing.Design.UITypeEditor
{
}
We must override GetEditStyle
and return the style we want to use.
public override System.Drawing.Design.UITypeEditorEditStyle
GetEditStyle(System.ComponentModel.ITypeDescriptorContext context)
{
if(context != null && context.Instance != null )
{
return UITypeEditorEditStyle.DropDown ;
}
return base.GetEditStyle (context);
}
We must also override GetPaintValueSupported
to be able to paint our own value.
public override bool
GetPaintValueSupported(System.ComponentModel.ITypeDescriptorContext context)
{
return true;
}
To initiate the editing of the property, we override EditValue
. If there is an ImageList
assigned, we create a new instance of ImageListPanel
and then use IWindowsFormsEditorService.DropDownControl
to display it in a dropdown. Before displaying the dropdown, we also add an event listener so that we can handle the user input.
public override object
EditValue(System.ComponentModel.ITypeDescriptorContext context,
IServiceProvider provider, object value)
{
wfes = (IWindowsFormsEditorService)
provider.GetService(typeof(IWindowsFormsEditorService));
if((wfes == null) || (context == null))
return null ;
ImageList imageList = GetImageList(context.Instance) ;
if ((imageList == null) || (imageList.Images.Count==0))
return -1 ;
m_imagePanel = new ImageListPanel();
m_imagePanel.BackgroundColor = Color.FromArgb(241,241,241);
m_imagePanel.BackgroundOverColor = Color.FromArgb(102,154,204);
m_imagePanel.HLinesColor = Color.FromArgb(182,189,210);
m_imagePanel.VLinesColor = Color.FromArgb(182,189,210);
m_imagePanel.BorderColor = Color.FromArgb(0,0,0);
m_imagePanel.EnableDragDrop = true;
m_imagePanel.Init(imageList,12,12,6,(int)value);
m_imagePanel.ItemClick += new ImageListPanelEventHandler(OnItemClicked);
m_selectedIndex = -1;
wfes.DropDownControl(m_imagePanel);
return (m_selectedIndex != -1) ? m_selectedIndex : (int) value ;
}
Finally, we need to override PaintValue
to paint our own value.
public override void PaintValue(System.Drawing.Design.PaintValueEventArgs pe)
{
int imageIndex = -1 ;
if(pe.Value != null)
{
try
{
imageIndex = (int)Convert.ToUInt16( pe.Value.ToString() ) ;
}
catch
{
}
}
if((pe.Context.Instance == null) || (imageIndex < 0))
return ;
ImageList imageList = GetImageList(pe.Context.Instance) ;
if((imageList == null) || (imageList.Images.Count == 0)
|| (imageIndex >= imageList.Images.Count))
return ;
pe.Graphics.DrawImage(imageList.Images[imageIndex],pe.Bounds);
}
When the custom type editor is done, all we need to do is add the Editor
attribute to the properties that should use it.
[TypeConverter(typeof(ImageTypeConverter))]
[Editor(typeof(MozBar.ImageMapEditor),typeof(System.Drawing.Design.UITypeEditor))]
[Description("Image for normal state.")]
public int Normal
{
get
{
return m_imageIndex;
}
set
{
if (value != m_imageIndex)
{
m_imageIndex = value;
if (m_item.ImageChanged!=null)
m_item.ImageChanged(this,
new ImageChangedEventArgs(itemState.Normal));
m_item.Invalidate();
}
}
}
Theme support
MozBar
implements basic theme support. If Theme
is enabled and visual themes are supported by the application, it will use colors from the currently active theme for its select and focus state.
Implementing theme support basically involves four steps:
- Checking if the system (and the application) supports themes.
- Setting up the functions we need to call in uxTheme.dll.
- Intercepting the
WM_THEMECHANGED
event.
- Getting what we need from the theme.
To find out if the system supports themes, we need to check which version of ComCtl32.dll is in use. This is done by using its DllGetVersion
function. This will return true
if visual styles are enabled either by using a manifest or Application.EnableVisualStyles()
. It will return false
if visual themes have been disabled for the application.
[StructLayout(LayoutKind.Sequential)]
public struct DLLVERSIONINFO
{
public int cbSize;
public int dwMajorVersion;
public int dwMinorVersion;
public int dwBuildNumber;
public int dwPlatformID;
}
[DllImport("Comctl32.dll", EntryPoint="DllGetVersion", ExactSpelling=true,
PreserveSig=false, CharSet=CharSet.Unicode)]
private static extern int DllGetVersion(ref DLLVERSIONINFO s);
public bool _IsAppThemed()
{
try
{
DLLVERSIONINFO version = new DLLVERSIONINFO();
version.cbSize = Marshal.SizeOf(typeof(DLLVERSIONINFO));
int ret = DllGetVersion(ref version);
if (version.dwMajorVersion >= 6)
return true;
else
return false;
}
catch (Exception)
{
return false;
}
}
Almost all theme related functionality is found in the uxTheme.dll. This is an unmanaged DLL and since Microsoft does not supply any managed wrapper, we need to use DllImport
to call the functions we are interested in. The uxTheme API and the constants needed to use it are described in the two header files UxTheme.h and TmSchema.h which are available in the Microsoft Platform SDK, or if you have VS2003 or VS2005 they should also be available in the ..\VC\PlatformSDK\Include directory. These are the functions used by MozBar
:
[DllImport("uxTheme.dll", EntryPoint="GetThemeColor", ExactSpelling=true,
PreserveSig=false, CharSet=CharSet.Unicode )]
private extern static void GetThemeColor (System.IntPtr hTheme,
int partID,
int stateID,
int propID,
out int color);
[DllImport( "uxtheme.dll", CharSet=CharSet.Unicode )]
private static extern IntPtr OpenThemeData( IntPtr hwnd, string classes );
[DllImport( "uxtheme.dll", EntryPoint="CloseThemeData", ExactSpelling=true,
PreserveSig=false, CharSet=CharSet.Unicode) ]
private static extern int CloseThemeData( IntPtr hwnd );
To intercept the WM_THEMECHANGED
event, we must override WndProc
and check the message.
protected override void WndProc(ref Message m)
{
base.WndProc (ref m);
switch (m.Msg)
{
case WM_THEMECHANGED:
{
if (Theme)
GetThemeColors();
break;
}
}
}
OK, so now we have our functions, we know when the theme has changed and if visual themes are supported. The only thing left to do is to get the theme related info we want and possibly adjust it to fit our needs.
private void GetThemeColors()
{
int EPB_HEADERBACKGROUND = 1;
int EPB_NORMALGROUPBACKGROUND = 5;
int TMT_GRADIENTCOLOR1 = 3810;
int TMT_GRADIENTCOLOR2 = 3811;
Color selectColor = new Color();
Color focusColor = new Color();
Color borderColor = new Color();
bool useSystemColors = false;
if (m_themeManager._IsAppThemed())
{
if (m_theme!=IntPtr.Zero)
m_themeManager._CloseThemeData(m_theme);
m_theme = m_themeManager._OpenThemeData(this.Handle,"EXPLORERBAR");
if (m_theme!=IntPtr.Zero)
{
selectColor = m_themeManager._GetThemeColor(m_theme,
EPB_HEADERBACKGROUND,1,TMT_GRADIENTCOLOR2);
focusColor = m_themeManager._GetThemeColor(m_theme,
EPB_NORMALGROUPBACKGROUND,1,TMT_GRADIENTCOLOR1);
borderColor = ControlPaint.Light(selectColor);
selectColor = ControlPaint.LightLight(selectColor);
focusColor = ControlPaint.LightLight(selectColor);
}
}
ItemColors.SelectedBorder = selectColor;
ItemColors.Divider = borderColor;
this.BorderColor = borderColor;
ItemColors.SelectedBackground = selectColor;
ItemColors.FocusBackground = focusColor;
ItemColors.FocusBorder = selectColor;
Invalidate();
}
Known problems/issues
- When scrolling, the border of the
MozPane
will not draw correctly, hopefully this will be fixed in an update.
- Since this control uses an
ImageList
, it suffers from the infamous ImageList bug that destroys the alpha channel for 32 bit images when you add images to the ImageList
in design mode. Workarounds for this could be to replace the ImageList
with a control like the ImageSet
by Tom Guinter (available here) or to add the images at run time. This is hopefully fixed in Visual Studio 2005 so it might not be worth the hassle.
History
- 14 September 2005 - Version 1.5.1.0
- Fixed problem when switching imagelists.
- 12 August 2005 - Version 1.5.0.0
- Selected items are now scrolled into view.
- Improved scrolling into view of items when using the keyboard.
- Scrollbars are now updated properly when docked on a resizable window.
- 9 July 2005 - Version 1.4.0.0
- Added more design time support by implementing
[DefaultValue]
.
- Fixed bug that made the color for selected border the same as the color for selected background when using themes.
- 2 June 2005 - Version 1.3.0.0
- Fixed bug that caused exceptions if the
ImageList
was disposed at runtime or removed in design mode.
- Added XP theme support (
Theme
property).
- Added keyboard support (move with arrow keys, Tab, and select with Enter/Space).
- Removed
AutoScroll
property.
- Automatic scrollbar if control contains more items than can be visible.
- Added scroll events (
VerticalScroll
and HorizontalScroll
).
- Items resize when scrollbar is visible or hidden.
- Changed enumeration names, to prevent name clashes with other components.
- 18 May 2005 - Version 1.2.0.0
- Fixed responsiveness bug introduced in 1.1.
- 16 May 2005 - Version 1.1.0.0
- Added mouse button notification to
MozItem.DoubleClick
event.
- Possible to reset image by typing "none" or "" (leaving it empty).
- Added
Font
property to MozPane
.
- Added
OnFontChanged
event handler to MozItem
.
- Added
SelectButton
property to MozPane
.
- Added text colors for selected and focus state.
- 1 May 2005 - Version 1.0.0.0
Conclusion
I hope this control can be of use to you. I'm sure there is plenty of room for improvements and added functionality, so if you have any ideas or suggestions please post a comment.