65.9K
CodeProject 正在变化。 阅读更多。
Home

MozBar

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (51投票s)

2005年5月1日

CPOL

9分钟阅读

viewsIcon

372859

downloadIcon

6624

一个灵活的工具栏,与Firefox选项对话框中的工具栏非常相似。

MozBar

引言

我启动这个项目是因为在我的其他项目中需要类似Firefox选项对话框中的工具栏。这就是名字的由来。我四处寻找,但找不到任何符合我需求的控件,所以我决定自己编写一个。

编写这个控件的另一个好处是,它让我有机会尝试一些用于控件创建的技术,即设计器、TypeConverters 和 TypeEditors。我不会深入代码细节(源代码就是为此而设的),但我会尝试提及一些所用到的技术。

我从Matthew Hall 的主题任务栏中获得了许多关于通用设计和集合使用的提示,我还借鉴了 John O' Byrne 出色的Imagelistpopup 控件,对其进行了轻微修改并将其用于我的图像下拉编辑器。

使用控件

与任何其他 .NET 控件一样,要在 IDE 中使用 MozBar 控件,您应将其添加到工具箱面板。这可以通过右键单击工具箱选项卡并选择“添加/移除项...”来完成,然后浏览到 MozBar 程序集并选中它。这将把所有 MozBar 控件添加到工具箱,以便可以将它们拖放到 Windows 窗体或控件上。

MozBar 程序集中有两个主要控件

  • MozPane
  • MozItem

MozItem 是定义工具栏项的控件。MozItem 必须包含在 MozPane 中。MozPaneMozItem 控件集合的容器,并提供添加或移除时自动放置和重新定位的功能。

MozPaneMozItem 实例添加到窗体后,选择它并查看其属性。

MozPane

MozPane 作为 MozBar 将包含的所有 MozItem 的容器。

有几个属性定义了 MozBar 的外观和行为

  • Style:设置 MozBar 的方向/布局,可设置为 VerticalHorizontal
  • ImageList:包含项目使用的图像的 ImageList。如果未设置此项,则无法为项目选择图像。
  • ItemColors:用于项目各种状态的颜色。
  • ItemBorderStyles:用于项目各种状态的边框样式。
  • Padding:指定项目与边框之间垂直和水平间距的属性。
  • Items:包含 MozBar 中项目的集合。
  • Toggle:确定是否可以切换项目的选中状态。如果设置为 false,则取消选择项目的唯一方法是选择另一个项目。
  • SelectedItems:返回选中项目的数量。
  • MaxSelectedItems:同时选中的最大项目数,这仅在 Toggle 设置为 true 时有效。如果 Togglefalse,则 MaxSelectedItems 始终为 1。
  • SelectButton:用于选择的鼠标按钮。
  • Theme:指示控件是否应使用主题颜色。如果启用且不支持视觉主题,则将使用系统颜色。

最常用的事件是

  • ItemClick:表示项目已被单击。
  • ItemDoubleClick:表示项目已被双击。
  • ItemGotFocus:表示项目已获得焦点。
  • ItemLostFocus:表示项目已失去焦点。
  • ItemSelected:表示项目已被选中。
  • ItemDeselected:表示项目已被取消选中,即已切换。此事件仅在 Toggletrue 时才能触发。

所有 eventArgs 都包含负责该事件的 MozItem。要检查哪个项目导致了事件,请使用 Tag 属性。

private void mozPane1_ItemSelected(object sender, Pabo.MozBar.MozItemEventArgs e)
{
    // Check the tag..
    switch(e.MozItem.Tag)
    {
        case "Save":
        {
            break;
        }
        case "Load":
        {
            break;
        }
    }
}

ItemClick 事件使用 ItemClickEventArgs,除了负责的 MozItem 之外,还包括包含所用按钮的 Button 属性。

要选择一个项目,请使用方法 SelectItem(int index)SelectItem(string tag),您可以选择使用索引或标签来标识项目。

MozItem

MozItem 是实际的工具栏项,可以通过从工具箱拖动到 MozPane 上,或通过使用 MozPane 控件中的 Controls 属性将其添加到 MozPane

MozItem 可以有三种可能的状态

  • 正常
  • Selected
  • 焦点

每个状态都可以有不同的图像、边框样式以及背景和边框的颜色。图像通过项目的 Images 属性设置;颜色和边框样式通过 MozPane 控件中的 ItemColorsItemBorderStyles 属性设置。

一个项目也可以有不同的样式,通过 ItemStyle 属性设置

  • 图片
  • TextAndPicture
  • 文本
  • Divider

当设置为 Divider 时,项目将显示为分隔符(类似于菜单中使用的分隔符),水平或垂直显示取决于 MozPaneStyle。如果使用 TextAndPicture,则可以通过 TextAlign 属性对 Text 进行对齐,可能的位置是 LeftTopRightBottom

嵌套属性

在控件中组织相关属性的一种好方法是将它们分组到嵌套结构中,控件中常见的例子是 SizeFont 属性。在 MozBar 中,此技术用于多个属性,其中包括 MozPane 中的 Padding 属性。

Padding property

为了实现这一点,我们需要做两件事。首先,我们需要一个包含我们想要分组的属性的类(PaddingCollection),其次,我们需要一个 TypeConverterPaddingCollectionTypeConverter)来正确显示属性。

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

}

该类非常简单,但重要的部分是属性。TypeConverter 属性用于将类型转换器分配给类。

[TypeConverter(typeof(PaddingCollectionTypeConverter))]
public class PaddingCollection
{
}

RefreshProperties 属性用于在分配该属性的属性更改时强制刷新属性。

[RefreshProperties(System.ComponentModel.RefreshProperties.All)]
public int Horizontal
{
}

由于这是一个嵌套属性,类型转换器应该继承自 ExpandableObjectConverter,我们需要重写四个方法。在 CanConvertToCanConvertFrom 中,我们需要确保我们可以转换为字符串和从字符串转换。实际的转换在 ConvertToConvertFrom 函数中完成。

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

}

剩下的就是创建一个 public 属性在 MozPane 中,该属性处理 PaddingCollection 类型的值,我们就完成了。

[Browsable(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());
        }
    }
}

自定义图像选择器

为控件提供自定义类型编辑器也是一件好事。MozItem 中的每个图像状态都使用一个自定义图像选择器,用于从 MozPane 中分配的 ImageList 中选择图像。

Padding property

为了实现这一点,我们必须创建自己的类型编辑器 (ImageMapEditor),它继承自 System.Drawing.Design.UITypeEditor

public class ImageMapEditor : System.Drawing.Design.UITypeEditor   
{
}

我们必须重写 GetEditStyle 并返回我们想要使用的样式。

public override System.Drawing.Design.UITypeEditorEditStyle 
         GetEditStyle(System.ComponentModel.ITypeDescriptorContext context)
{
    if(context != null && context.Instance != null ) 
    {
        return UITypeEditorEditStyle.DropDown ;
    }
    return base.GetEditStyle (context);
}

我们还必须重写 GetPaintValueSupported 才能绘制自己的值。

public override bool 
       GetPaintValueSupported(System.ComponentModel.ITypeDescriptorContext context)
{
    return true;
}

要启动属性编辑,我们重写 EditValue。如果分配了 ImageList,我们创建一个 ImageListPanel 的新实例,然后使用 IWindowsFormsEditorService.DropDownControl 以下拉方式显示它。在显示下拉列表之前,我们还会添加一个事件监听器,以便我们可以处理用户输入。

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

最后,我们需要重写 PaintValue 来绘制我们自己的值。

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

自定义类型编辑器完成后,我们只需将 Editor 属性添加到应使用它的属性即可。

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

主题支持

MozBar 实现了基本的主题支持。如果启用了 Theme 并且应用程序支持视觉主题,它将使用当前活动主题的颜色来表示其选择和焦点状态。

LunaSilverOlive

实现主题支持基本上包括四个步骤

  • 检查系统(和应用程序)是否支持主题。
  • 设置我们需要在 uxTheme.dll 中调用的函数。
  • 拦截 WM_THEMECHANGED 事件。
  • 从主题中获取我们需要的信息。

要查明系统是否支持主题,我们需要检查正在使用的 ComCtl32.dll 版本。这是通过使用其 DllGetVersion 函数完成的。如果通过使用清单或 Application.EnableVisualStyles() 启用了视觉样式,它将返回 true。如果已为应用程序禁用了视觉主题,它将返回 false

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

几乎所有与主题相关的功能都可以在 uxTheme.dll 中找到。这是一个非托管 DLL,由于 Microsoft 不提供任何托管包装器,我们需要使用 DllImport 来调用我们感兴趣的函数。uxTheme API 和使用它所需的常量在两个头文件 UxTheme.hTmSchema.h 中描述,这些文件可在 Microsoft Platform SDK 中找到,或者如果您有 VS2003 或 VS2005,它们也应该在 ..\VC\PlatformSDK\Include 目录中。这些是 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 );

要拦截 WM_THEMECHANGED 事件,我们必须重写 WndProc 并检查消息。

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

好的,现在我们有了函数,我们知道主题何时更改以及是否支持视觉主题。剩下要做的就是获取我们想要的主题相关信息,并可能对其进行调整以适应我们的需求。

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

}

已知问题

  • 滚动时,MozPane 的边框将无法正确绘制,希望这将在更新中修复。
  • 由于此控件使用了 ImageList,它受到了臭名昭著的 ImageList 错误的困扰,该错误在设计模式下向 ImageList 添加图像时会破坏 32 位图像的 Alpha 通道。解决此问题的方法可以是使用 Tom Guinter 的 ImageSet 控件(可在此处获得)替换 ImageList,或在运行时添加图像。希望这在 Visual Studio 2005 中已修复,因此可能不值得费心。

历史

  • 2005 年 9 月 14 日 - 版本 1.5.1.0
    • 修复了切换图像列表时的问题。
  • 2005 年 8 月 12 日 - 版本 1.5.0.0
    • 现在选定的项目会滚动到视图中。
    • 在使用键盘时,改进了项目滚动到视图中的效果。
    • 当停靠在可调整大小的窗口上时,滚动条现在会正确更新。
  • 2005 年 7 月 9 日 - 版本 1.4.0.0
    • 通过实现 [DefaultValue] 增加了更多设计时支持。
    • 修复了在使用主题时,选中边框颜色与选中背景颜色相同的问题。
  • 2005 年 6 月 2 日 - 版本 1.3.0.0
    • 修复了在运行时处置或在设计模式下移除 ImageList 会导致异常的错误。
    • 添加了 XP 主题支持 (Theme 属性)。
    • 添加了键盘支持(使用箭头键、Tab 键移动,使用 Enter/Space 键选择)。
    • 移除了 AutoScroll 属性。
    • 如果控件包含的项目多于可见项目,则自动显示滚动条。
    • 添加了滚动事件 (VerticalScrollHorizontalScroll)。
    • 当滚动条可见或隐藏时,项目会调整大小。
    • 更改了枚举名称,以防止与其他组件发生名称冲突。
  • 2005 年 5 月 18 日 - 版本 1.2.0.0
    • 修复了 1.1 中引入的响应性错误。
  • 2005 年 5 月 16 日 - 版本 1.1.0.0
    • MozItem.DoubleClick 事件添加了鼠标按钮通知。
    • 可以通过输入“none”或“”来重置图像(留空)。
    • MozPane 添加了 Font 属性。
    • MozItem 添加了 OnFontChanged 事件处理程序。
    • MozPane 添加了 SelectButton 属性。
    • 为选中和焦点状态添加了文本颜色。
  • 2005 年 5 月 1 日 - 版本 1.0.0.0
    • 首次发布。

结论

我希望这个控件能对您有所帮助。我相信还有很多改进和添加功能的空间,所以如果您有任何想法或建议,请发表评论。

© . All rights reserved.