<think>我们创建一个自定义的TabControl,名为CustomTabControl,继承自TabControl。
要求:
1. 支持设计时正常显示(即设计时添加的控件在重新生成后不会消失)。
2. 支持分页按钮(即TabPage的标签头)背景色改变和大小改变。
3. 支持选中页面按钮下划线(即当前选中的TabPage标签头下方显示一条下划线)。
实现思路:
1. 设计时支持:确保正确序列化TabPages属性,并添加设计器支持。
2. 自定义绘制:通过OwnerDrawFixed模式,重写OnDrawItem方法,实现自定义绘制标签头。
3. 属性扩展:添加自定义属性,如SelectedUnderlineColor, TabButtonBackColor, TabButtonSize等。
4. 调整标签头大小:通过重写OnSizeChanged或使用ItemSize属性,但注意自定义绘制时可能需要自己控制大小。
注意:为了设计时支持,我们将使用DesignerSerializationVisibility和DesignerAttribute。
步骤:
1. 创建CustomTabControl类,继承TabControl。
2. 设置OwnerDraw = true,并设置DrawMode为OwnerDrawFixed(或OwnerDrawVariable,这里使用Fixed)。
3. 重写OnDrawItem方法,绘制每个标签头。
4. 添加自定义属性,并在属性变化时刷新(Invalidate)。
5. 确保序列化。
但是注意:直接改变标签头大小,我们可以通过设置ItemSize属性。但是为了允许每个标签头可以有不同的背景色,我们需要在绘制时处理。
由于TabControl的标签头默认是统一大小,我们这里不改变每个标签头的大小(即保持ItemSize统一),但允许通过属性设置整个控件的标签头大小(通过ItemSize设置)。如果每个标签头要不同大小,则需要使用OwnerDrawVariable,并重写MeasureItem,但这样更复杂,且设计时支持可能更难。根据要求,我们支持大小改变,可以整体改变,所以通过ItemSize即可。
下划线效果:在选中的标签头底部绘制一条有颜色的线。
自定义属性:
- SelectedUnderlineColor: 选中标签的下划线颜色
- TabButtonBackColor: 所有标签头的背景色(非选中)
- SelectedTabButtonBackColor: 选中标签头的背景色
注意:我们还需要考虑默认情况下,设计时添加的控件能够被正确序列化。
代码结构:
我们将添加以下属性:
[Browsable(true)]
[Category("Appearance")]
[Description("获取或设置选中标签下划线的颜色")]
public Color SelectedUnderlineColor { get; set; } = Color.Red;
[Browsable(true)]
[Category("Appearance")]
[Description("获取或设置非选中标签的背景色")]
public Color TabButtonBackColor { get; set; } = SystemColors.Control;
[Browsable(true)]
[Category("Appearance")]
[Description("获取或设置选中标签的背景色")]
public Color SelectedTabButtonBackColor { get; set; } = SystemColors.ControlLight;
注意:由于我们使用了OwnerDraw,所以需要自己绘制背景和文本。同时,我们也要绘制边框等。
另外,为了设计时支持,我们需要:
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public new TabPageCollection TabPages => base.TabPages;
但是,TabPageCollection在基类中已经标记为DesignerSerializationVisibility.Content,所以我们可以不重写。但是为了确保设计时正确,我们也可以显式重写。
然而,我们自定义的控件在重新生成后设计时子控件消失的问题,通常是由于设计器序列化问题。我们按照之前的方法,添加设计器属性:
[Designer("System.Windows.Forms.Design.TabControlDesigner, System.Design, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")]
public class CustomTabControl : TabControl
由于我们自定义绘制,所以还需要处理DrawItem事件(或重写OnDrawItem)。
具体绘制步骤:
- 在OnDrawItem中,获取当前标签的TabPage和标签区域。
- 根据是否选中,选择背景色。
- 填充背景(矩形区域)。
- 绘制文本(注意文本对齐和颜色)。
- 如果选中,则在底部绘制一条下划线(在标签头的底部,而不是整个控件的底部)。
注意:标签头区域不包括边框,所以下划线应该绘制在标签头矩形的底部。
另外,我们还需要考虑控件的显示效果,比如边框。默认情况下,TabControl有边框,我们可以在自定义绘制时绘制边框,或者使用系统默认边框。这里为了简单,我们使用系统默认边框,只自定义标签头。
但是,当DrawMode=OwnerDrawFixed时,整个控件的绘制(包括边框)都需要自己处理。所以我们需要在OnDrawItem中绘制标签头,而边框则可以通过重写OnPaint来绘制整个控件的边框,但这样比较复杂。
另一种做法:我们只绘制标签头,边框保留系统默认。我们可以设置DrawMode=OwnerDrawFixed,然后只处理标签头的绘制,边框让系统绘制。但是,在OwnerDraw模式下,边框的绘制可能会被覆盖。所以我们需要在自定义绘制标签头时,注意不要覆盖边框。
实际上,TabControl的边框是由一个矩形区域包围的,标签头在边框的上部。在绘制标签头时,我们绘制的区域是标签头区域,不会影响边框。
因此,我们这样实现:
- 设置DrawMode = OwnerDrawFixed。
- 重写OnDrawItem,绘制每个标签头(背景、文本、下划线)。
- 在绘制标签头时,我们使用TabItemState来区分状态(如选中、正常等)。
但是,我们还需要考虑鼠标悬停等状态,但题目未要求,所以只处理选中和非选中。
另外,为了改变标签头的大小,我们可以在属性中暴露一个TabButtonSize属性,然后将其赋值给ItemSize。注意:ItemSize是基类属性,我们可以在设计时设置。
但是,我们这里不添加新的属性来替代ItemSize,而是直接让用户使用ItemSize属性(因为ItemSize本身就在设计时可用)。所以,我们不需要额外添加TabButtonSize属性,直接使用基类的ItemSize即可。
因此,我们只需要添加背景色和下划线颜色属性。
开始编写代码:
注意:由于自定义绘制,我们需要设置一些默认值,比如在构造函数中设置DrawMode和OwnerDraw。
另外,当属性值改变时,需要调用Invalidate()重绘。
代码示例:
</think>
以下是支持设计时正常显示、自定义样式和下划线效果的完整自定义 `TabControl` 实现:
```csharp
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
using System.Windows.Forms.Design;
// 必须添加设计器支持
[Designer(typeof(TabControlDesigner))]
public class CustomTabControl : TabControl
{
// 自定义属性
[Category("Custom")]
public Color SelectedTabUnderlineColor { get; set; } = Color.Red;
[Category("Custom")]
public Color TabButtonBackColor { get; set; } = SystemColors.Control;
[Category("Custom")]
public Color SelectedTabButtonBackColor { get; set; } = Color.LightBlue;
[Category("Custom")]
public Size TabButtonSize { get; set; } = new Size(100, 30);
public CustomTabControl()
{
// 启用自定义绘制
this.DrawMode = TabDrawMode.OwnerDrawFixed;
this.SizeMode = TabSizeMode.Fixed;
this.ItemSize = TabButtonSize;
// 确保设计时序列化
SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
}
// 关键:确保序列化 TabPages 内容
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public new TabPageCollection TabPages => base.TabPages;
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
DrawCustomTabs(e.Graphics);
}
private void DrawCustomTabs(Graphics g)
{
for (int i = 0; i < TabCount; i++)
{
Rectangle tabRect = GetTabRect(i);
bool isSelected = (SelectedIndex == i);
// 绘制标签背景
using (Brush backBrush = new SolidBrush(
isSelected ? SelectedTabButtonBackColor : TabButtonBackColor))
{
g.FillRectangle(backBrush, tabRect);
}
// 绘制标签文本
TextRenderer.DrawText(
g,
TabPages[i].Text,
Font,
tabRect,
ForeColor,
TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter
);
// 绘制选中状态的下划线
if (isSelected)
{
using (Pen underlinePen = new Pen(SelectedTabUnderlineColor, 3))
{
int underlineY = tabRect.Bottom - 2;
g.DrawLine(underlinePen, tabRect.Left, underlineY, tabRect.Right, underlineY);
}
}
}
}
}
// 设计器支持类(必需)
internal class TabControlDesigner : ParentControlDesigner
{
public override void Initialize(IComponent component)
{
base.Initialize(component);
if (Control is CustomTabControl tabControl)
{
tabControl.DrawMode = TabDrawMode.Normal; // 设计模式禁用自定义绘制
}
}
protected override void PreFilterProperties(System.Collections.IDictionary properties)
{
base.PreFilterProperties(properties);
// 确保TabPages属性在设计器中可见
properties["TabPages"] = TypeDescriptor.CreateProperty(
typeof(CustomTabControl),
(PropertyDescriptor)properties["TabPages"],
new Attribute[] { new DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Content) }
);
}
}
```
### 使用说明
1. **设计时支持**:
- 通过 `TabControlDesigner` 类确保设计器能正确处理控件
- `DesignerSerializationVisibility` 特性保证子控件序列化
2. **自定义属性**:
```csharp
// 设置标签按钮大小
customTabControl1.TabButtonSize = new Size(120, 40);
// 设置选中标签下划线颜色
customTabControl1.SelectedTabUnderlineColor = Color.Green;
// 设置标签背景色
customTabControl1.TabButtonBackColor = Color.LightGray;
customTabControl1.SelectedTabButtonBackColor = Color.White;
```
3. **设计时注意事项**:
- 在 VS 工具箱右键 → 选择项 → 浏览添加编译后的 DLL
- 拖拽到窗体后,直接在属性窗口修改自定义属性
- 在 TabPage 中添加的子控件会自动持久化到 `.Designer.cs`
4. **运行效果**:
- 非选中标签:灰色背景
- 选中标签:白色背景 + 绿色下划线
- 所有标签高度 40px,宽度 120px
### 验证设计时持久化
在 `.Designer.cs` 中应看到如下代码:
```csharp
// 自动生成的子控件代码
this.customTabControl1.TabPages.AddRange(new TabPage[] {
tabPage1, tabPage2});
this.tabPage1.Controls.Add(this.button1);
this.tabPage1.Controls.Add(this.label1);
```