Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

MozBar

0.00/5 (No votes)
29 Sep 2005 1  
A flexible toolbar very much like the toolbar in FireFox Options dialog.

MozBar

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, TypeConverters and TypeEditors. 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:

  • MozPane
  • MozItem

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 MozItems 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 eventArgss 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)
{
    // Check the tag..

    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:

  • Normal
  • Selected
  • Focus

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.

Padding property

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)
    {
        // set the control to which the collection belong

        m_pane = pane;
        // Default values

        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)
            {
                // padding has changed , force DoLayout

                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))
        {
            // Parse property string

            string[] ss = value.ToString().Split(new char[] {';'}, 2);
            if (ss.Length==2)
            {
                // Create new PaddingCollection

                PaddingCollection item = 
                   new PaddingCollection((MozPane)context.Instance); 
                // Set properties

                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) )
        {
            // cast value to paddingCollection

            PaddingCollection dest = (PaddingCollection)value;  
            // create property string

            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.

Padding property

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);

    // add listner for event

    m_imagePanel.ItemClick += new ImageListPanelEventHandler(OnItemClicked);

    // set m_selectedIndex to -1 in case

    // the dropdown is closed without selection

    m_selectedIndex = -1;
    // show the popup as a drop-down

    wfes.DropDownControl(m_imagePanel);

    // return the selection (or the original value if none selected)

    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 ;
    // value is the image index

    if(pe.Value != null) 
    {
        try 
        {
            imageIndex = (int)Convert.ToUInt16( pe.Value.ToString() ) ;
        }
        catch
        {
        }
    }
    // no instance, or the instance represents an undefined image

    if((pe.Context.Instance == null) || (imageIndex < 0))
        return ;
    // get the image set

    ImageList imageList = GetImageList(pe.Context.Instance) ;
    // make sure everything is valid

    if((imageList == null) || (imageList.Images.Count == 0) 
              || (imageIndex >= imageList.Images.Count))
        return ;
    // Draw the preview image

    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.

LunaSilverOlive

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.

// Setup struct and function call


[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
    {
        // Check which version of ComCtl32 thats in use..

        DLLVERSIONINFO version = new DLLVERSIONINFO();
        version.cbSize = Marshal.SizeOf(typeof(DLLVERSIONINFO));
        
        int ret = DllGetVersion(ref version);
        // If MajorVersion > 5 themes are allowed.

        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:
        {
            // Theme has changed , get new colors if Theme = true

            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;

    // Check if themes are available

    if (m_themeManager._IsAppThemed())
    {
        if (m_theme!=IntPtr.Zero)
          m_themeManager._CloseThemeData(m_theme); 

        // Open themes for "ExplorerBar"

        m_theme = m_themeManager._OpenThemeData(this.Handle,"EXPLORERBAR");  
        if (m_theme!=IntPtr.Zero)
        {
            // Get Theme colors..

            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);
        }
    }

    // apply colors..

    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
    • First release.

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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here