MozBar






4.85/5 (51投票s)
一个灵活的工具栏,与Firefox选项对话框中的工具栏非常相似。
引言
我启动这个项目是因为在我的其他项目中需要类似Firefox选项对话框中的工具栏。这就是名字的由来。我四处寻找,但找不到任何符合我需求的控件,所以我决定自己编写一个。
编写这个控件的另一个好处是,它让我有机会尝试一些用于控件创建的技术,即设计器、TypeConverter
s 和 TypeEditor
s。我不会深入代码细节(源代码就是为此而设的),但我会尝试提及一些所用到的技术。
我从Matthew Hall 的主题任务栏中获得了许多关于通用设计和集合使用的提示,我还借鉴了 John O' Byrne 出色的Imagelistpopup 控件,对其进行了轻微修改并将其用于我的图像下拉编辑器。
使用控件
与任何其他 .NET 控件一样,要在 IDE 中使用 MozBar
控件,您应将其添加到工具箱面板。这可以通过右键单击工具箱选项卡并选择“添加/移除项...”来完成,然后浏览到 MozBar
程序集并选中它。这将把所有 MozBar
控件添加到工具箱,以便可以将它们拖放到 Windows 窗体或控件上。
MozBar
程序集中有两个主要控件
MozPane
MozItem
MozItem
是定义工具栏项的控件。MozItem
必须包含在 MozPane
中。MozPane
是 MozItem
控件集合的容器,并提供添加或移除时自动放置和重新定位的功能。
将 MozPane
或 MozItem
实例添加到窗体后,选择它并查看其属性。
MozPane
MozPane
作为 MozBar
将包含的所有 MozItem
的容器。
有几个属性定义了 MozBar
的外观和行为
Style
:设置MozBar
的方向/布局,可设置为Vertical
或Horizontal
。ImageList
:包含项目使用的图像的ImageList
。如果未设置此项,则无法为项目选择图像。ItemColors
:用于项目各种状态的颜色。ItemBorderStyles
:用于项目各种状态的边框样式。Padding
:指定项目与边框之间垂直和水平间距的属性。Items
:包含MozBar
中项目的集合。Toggle
:确定是否可以切换项目的选中状态。如果设置为false
,则取消选择项目的唯一方法是选择另一个项目。SelectedItems
:返回选中项目的数量。MaxSelectedItems
:同时选中的最大项目数,这仅在Toggle
设置为true
时有效。如果Toggle
为false
,则MaxSelectedItems
始终为 1。SelectButton
:用于选择的鼠标按钮。Theme
:指示控件是否应使用主题颜色。如果启用且不支持视觉主题,则将使用系统颜色。
最常用的事件是
ItemClick
:表示项目已被单击。ItemDoubleClick
:表示项目已被双击。ItemGotFocus
:表示项目已获得焦点。ItemLostFocus
:表示项目已失去焦点。ItemSelected
:表示项目已被选中。ItemDeselected
:表示项目已被取消选中,即已切换。此事件仅在Toggle
为true
时才能触发。
所有 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
控件中的 ItemColors
和 ItemBorderStyles
属性设置。
一个项目也可以有不同的样式,通过 ItemStyle
属性设置
图片
TextAndPicture
文本
Divider
当设置为 Divider
时,项目将显示为分隔符(类似于菜单中使用的分隔符),水平或垂直显示取决于 MozPane
的 Style
。如果使用 TextAndPicture
,则可以通过 TextAlign
属性对 Text
进行对齐,可能的位置是 Left
、Top
、Right
或 Bottom
。
嵌套属性
在控件中组织相关属性的一种好方法是将它们分组到嵌套结构中,控件中常见的例子是 Size
和 Font
属性。在 MozBar
中,此技术用于多个属性,其中包括 MozPane
中的 Padding
属性。
为了实现这一点,我们需要做两件事。首先,我们需要一个包含我们想要分组的属性的类(PaddingCollection
),其次,我们需要一个 TypeConverter
(PaddingCollectionTypeConverter
)来正确显示属性。
[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
,我们需要重写四个方法。在 CanConvertTo
和 CanConvertFrom
中,我们需要确保我们可以转换为字符串和从字符串转换。实际的转换在 ConvertTo
和 ConvertFrom
函数中完成。
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
中选择图像。
为了实现这一点,我们必须创建自己的类型编辑器 (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
并且应用程序支持视觉主题,它将使用当前活动主题的颜色来表示其选择和焦点状态。
实现主题支持基本上包括四个步骤
- 检查系统(和应用程序)是否支持主题。
- 设置我们需要在 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.h 和 TmSchema.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
属性。 - 如果控件包含的项目多于可见项目,则自动显示滚动条。
- 添加了滚动事件 (
VerticalScroll
和HorizontalScroll
)。 - 当滚动条可见或隐藏时,项目会调整大小。
- 更改了枚举名称,以防止与其他组件发生名称冲突。
- 修复了在运行时处置或在设计模式下移除
- 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
- 首次发布。
结论
我希望这个控件能对您有所帮助。我相信还有很多改进和添加功能的空间,所以如果您有任何想法或建议,请发表评论。