如何在 Winforms 中更改选项卡控件的背景颜色

本文关键字:控件 背景 颜色 选项 Winforms | 更新日期: 2023-09-27 17:48:55

有没有办法在winforms中更改选项卡控件的背景颜色,使其周围没有白色边框?

我尝试了几种不同的方法,但它们都会导致显示相同的白色边框。

如何在 Winforms 中更改选项卡控件的背景颜色

TabControl

自定义的支持非常差。我已经成功地使用了这个自定义选项卡控件。如果您想像我一样更改外观,该代码非常有用。

我只能考虑将外观属性更改为按钮

MSDN 选项卡控件外观

首先,您需要从 TabControl 创建一个派生类。到目前为止一切顺利,但现在它变脏了。

因为 TabControl 不会调用 OnPaint ,所以我们必须重写WndProc来处理WM_PAINT消息。在那里,我们继续用我们喜欢的颜色绘制我们的背景。

 protected override void WndProc(ref Message m)
    {
        base.WndProc(ref m);
        if(m.Msg == (int) WindowsMessages.Win32Messages.WM_PAINT)
        {
            using (Graphics g = this.CreateGraphics())
            {
                //Double buffering stuff...
                BufferedGraphicsContext currentContext;
                BufferedGraphics myBuffer;
                currentContext = BufferedGraphicsManager.Current;
                myBuffer = currentContext.Allocate(g,
                   this.ClientRectangle);
                Rectangle r = ClientRectangle;
                //Painting background
                if(Enabled)
                    myBuffer.Graphics.FillRectangle(new SolidBrush(_backColor), r);
                else
                    myBuffer.Graphics.FillRectangle(Brushes.LightGray, r);
                //Painting border
                r.Height = this.DisplayRectangle.Height +1; //Using display rectangle hight because it excludes the tab headers already
                r.Y = this.DisplayRectangle.Y - 1; //Same for Y coordinate
                r.Width -= 5;
                r.X += 1;
                if(Enabled)
                    myBuffer.Graphics.DrawRectangle(new Pen(Color.FromArgb(255, 133, 158, 191), 1), r);
                else
                    myBuffer.Graphics.DrawRectangle(Pens.DarkGray, r);
                myBuffer.Render();
                myBuffer.Dispose();
                //Actual painting of items after Background was painted
                foreach (int index in ItemArgs.Keys)
                {
                    CustomDrawItem(ItemArgs[index]);
                }
            }
        }    
    }

我在这种方法中做了进一步的绘图,所以对于这个问题看起来有点矫枉过正,但只是忽略不必要的东西。另请注意foreach循环。我稍后会谈到这一点。

问题是TabControl在自己的WM_PAINT之前绘制其项目(选项卡标题),因此我们的背景将绘制在顶部,从而使它们不可见。为了解决这个问题,我为DrawItem做了一个EventHandler,如下所示:

    private void DrawItemHandler(object sender, DrawItemEventArgs e)
    {
        //Save information about item in dictionary but dont do actual drawing
        if (!ItemArgs.ContainsKey(e.Index))
            ItemArgs.Add(e.Index, e);
        else
            ItemArgs[e.Index] = e;
    }

我正在将DrawItemEventArgs保存到字典中(在我的情况下称为"ItemArgs"),以便以后可以访问它们。这就是几秒钟前的foreach发挥作用的地方。它调用一个方法,我在其中绘制选项卡标题,该方法将我们之前保存的DrawItemEventArgs作为参数来绘制正确的状态和位置的项目。

因此,简而言之,我们正在拦截选项卡标题的绘制以延迟它,直到我们完成绘制背景。

此解决方案不是最佳的,但它有效,并且这是您唯一可以做的,可以在不从头开始绘制的情况下更好地控制TabControl(笑)。

添加到

@janhildebrandt答案,因为它缺少一些实际使其工作的关键部分。

性能

TabControlDrawMode 属性必须设置为 TabDrawMode.OwnerDrawFixed否则DrawItem事件处理程序将不会触发。

只需重写派生的 TabControl 类中的属性,如下所示:

public new TabDrawMode DrawMode
{
    get
    {
        return TabDrawMode.OwnerDrawFixed;
    }
    set
    {
        // No you dont.
    }
}

public MyTabControl() 
{
    base.DrawMode = TabDrawMode.OwnerDrawFixed;
}
<小时 />

DrawItemEventArgs

我不知道代码是在哪个版本中编写的,但是在 2023 年 .Net 4.5 及更高版本下存储TabItemGraphics对象将不起作用,也不需要。

相反,请考虑使用如下结构:

private struct TabItemInfo
{
    public Color            BackColor;
    public Rectangle        Bounds;
    public Font             Font;
    public Color            ForeColor;
    public int              Index;
    public DrawItemState    State;
    public TabItemInfo(DrawItemEventArgs e)
    {
        this.BackColor  = e.BackColor;
        this.ForeColor  = e.ForeColor;
        this.Bounds     = e.Bounds;
        this.Font       = e.Font;
        this.Index      = e.Index;
        this.State      = e.State;
    }
}
private Dictionary<int, TabItemInfo> _tabItemStateMap = new Dictionary<int, TabItemInfo>();

DrawItem 事件处理程序

如果已从控件本身派生,请不要分配事件处理程序。请改用OnDrawItem(DrawItemEventArgs)方法:

protected override void OnDrawItem(DrawItemEventArgs e)
{
    base.OnDrawItem(e);
    if (!_tabItemStateMap.ContainsKey(e.Index)) 
    {
        _tabItemStateMap.Add(e.Index, new TabItemInfo(e));
    }
    else
    {
        _tabItemStateMap[e.Index] = new TabItemInfo(e);
    }
}
<小时 />

WndProc 中的改进

您的TabControl将在设计模式下闪烁.
通过检查WM_ERASEBKGND消息,可以轻松避免这种情况。只需在DesignMode期间省略它:

private const int WM_PAINT      = 0x000F;
private const int WM_ERASEBKGND = 0x0014;        
// Cache context to avoid repeatedly re-creating the object.
// WM_PAINT is called frequently so it's better to declare it as a member.
private BufferedGraphicsContext _bufferContext = BufferedGraphicsManager.Current;
protected override void WndProc(ref Message m)
{            
    switch (m.Msg)
    {
        case WM_PAINT:
            {
                // Let system do its thing first.
                base.WndProc(ref m);
                
                // Custom paint Tab items.
                HandlePaint(ref m);
                break;
            }                    
        case WM_ERASEBKGND:
            {
                if (DesignMode)
                {
                    // Ignore to prevent flickering in DesignMode.
                }
                else
                {
                    base.WndProc(ref m);
                }
                break;
            }                    
        default:
            base.WndProc(ref m);
            break;
    }
}

private Color _backColor = Color.FromArgb(31, 31, 31);
[Browsable(true)]
[EditorBrowsable(EditorBrowsableState.Always)]
public new Color BackColor
{
    get
    {
        return _backColor;
    }
    set
    {
        _backColor = value;
    }
}

private void HandlePaint(ref Message m)
{
    using (var g = Graphics.FromHwnd(m.HWnd))
    {
        SolidBrush backBrush = new SolidBrush(BackColor);
        Rectangle r = ClientRectangle;
        using (var buffer = _bufferContext.Allocate(g, r))
        {                    
            if (Enabled)
            {
                buffer.Graphics.FillRectangle(backBrush, r);
            }
            else
            {
                buffer.Graphics.FillRectangle(backBrush, r);
            }
            // Paint items
            foreach (int index in _tabItemStateMap.Keys)
            {
                DrawTabItemInternal(buffer.Graphics, _tabItemStateMap[index]);
            }
            buffer.Render();
        }
        backBrush.Dispose();        
    }
}

private void DrawTabItemInternal(Graphics gr, TabItemInfo tabInfo) 
{
    /* Uncomment the two lines below to have each TabItem use the same height.
    ** The selected TabItem height will be slightly taller
    ** which makes unselected tabs float if you choose to 
    ** have a different BackColor for the TabControl background
    ** and your TabItem background. 
    */  
    
    // int fullHeight = _tabItemStateMap[this.SelectedIndex].Bounds.Height;
    // tabInfo.Bounds.Height = fullHeight;
    
    SolidBrush backBrush = new SolidBrush(BackColor);
    
    // Paint selected. 
    // You might want to choose a different color for the 
    // background or the text.
    if ((tabInfo.State & DrawItemState.Selected) == DrawItemState.Selected)
    {
        gr.FillRectangle(backBrush, tabInfo.Bounds);
        gr.DrawString(this.TabPages[tabInfo.Index].Text, tabInfo.Font, 
            SystemBrushes.ControlText, tabInfo.Bounds);
    }
    // Paint unselected.
    else
    {
        gr.FillRectangle(backBrush, tabInfo.Bounds);
        gr.DrawString(this.TabPages[tabInfo.Index].Text, tabInfo.Font, 
            SystemBrushes.ControlText, tabInfo.Bounds);
    }
    
    backBrush.Dispose();
}
<小时 />

进一步的改进

实心画笔

与其重新创建SolidBrush对象,不如考虑将它们声明为类的成员。

例:

private SolidBrush _backBrush;
private SolidBrush _tabBackBrush;
private SolidBrush _tabForeBrush;

private Color _tabBackColor = Color.FromArgb(31, 31, 31);
public Color TabBackColor
{
    get
    {
        return _tabBackColor;
    }
    set
    {
        _tabBackColor = value;
        _tabBackBrush?.Dispose();
        _tabBackBrush = new SolidBrush(_tabBackColor);
    }
}

private Color _tabForeColor = Color.FromArgb(241, 241, 241);
public Color TabForeColor
{
    get
    {
        return _tabForeColor;
    }
    set
    {
        _tabForeColor = value;
        _tabForeBrush?.Dispose();
        _tabForeBrush = new SolidBrush(_tabForeColor);
    }
}
private Color _backColor = Color.FromArgb(31, 31, 31);
[Browsable(true)]
[EditorBrowsable(EditorBrowsableState.Always)]
public new Color BackColor
{
    get
    {
        return _backColor;
    }
    set
    {
        _backColor = value;
        _backBrush?.Dispose();
        _backBrush = new SolidBrush(_backColor);
    }
}
protected override void Dispose(bool disposing)
{
    _backBrush.Dispose();
    _tabBackBrush.Dispose();
    _tabForeBrush.Dispose();
    
    base.Dispose(disposing);
}

设置样式

使用ControlStyles.OptimizedDoubleBuffer可能会进一步减少闪烁(如果有的话)。

public MyTabControl() 
{
    this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
}
<小时 />

特征

选项卡项中的文本对齐方式

在绘制 TabItem 的文本时传递StringFormat对象以定位文本,就像使用Label一样

private StringFormat _tabTextFormat = new StringFormat();

private void UpdateTextAlign()
{
    switch (this.TextAlign)
    {
        case ContentAlignment.TopLeft:
            _tabTextFormat.Alignment = StringAlignment.Near;
            _tabTextFormat.LineAlignment = StringAlignment.Near;
            break;
        case ContentAlignment.TopCenter:
            _tabTextFormat.Alignment = StringAlignment.Center;
            _tabTextFormat.LineAlignment = StringAlignment.Near;
            break;
        case ContentAlignment.TopRight:
            _tabTextFormat.Alignment = StringAlignment.Far;
            _tabTextFormat.LineAlignment = StringAlignment.Near;
            break;
        case ContentAlignment.MiddleLeft:
            _tabTextFormat.Alignment = StringAlignment.Near;
            _tabTextFormat.LineAlignment = StringAlignment.Center;
            break;
        case ContentAlignment.MiddleCenter:
            _tabTextFormat.Alignment = StringAlignment.Center;
            _tabTextFormat.LineAlignment = StringAlignment.Center;
            break;
        case ContentAlignment.MiddleRight:
            _tabTextFormat.Alignment = StringAlignment.Far;
            _tabTextFormat.LineAlignment = StringAlignment.Center;
            break;
        case ContentAlignment.BottomLeft:
            _tabTextFormat.Alignment = StringAlignment.Near;
            _tabTextFormat.LineAlignment = StringAlignment.Far;
            break;
        case ContentAlignment.BottomCenter:
            _tabTextFormat.Alignment = StringAlignment.Center;
            _tabTextFormat.LineAlignment = StringAlignment.Far;
            break;
        case ContentAlignment.BottomRight:
            _tabTextFormat.Alignment = StringAlignment.Far;
            _tabTextFormat.LineAlignment = StringAlignment.Far;
            break;
    }
}

private ContentAlignment _textAlign = ContentAlignment.TopLeft;
public ContentAlignment TextAlign
{
    get
    {
        return _textAlign;
    }
    set
    {
        if (value != _textAlign)
        {
            _textAlign = value;
            UpdateTextAlign();
        }                
    }
}

private void DrawTabItemInternal(Graphics gr, TabItemInfo tabInfo) 
{
    if ((tabInfo.State & DrawItemState.Selected) == DrawItemState.Selected)
    {
        gr.FillRectangle(_tabBackBrush, tabInfo.Bounds);
        gr.DrawString(this.TabPages[tabInfo.Index].Text, tabInfo.Font, 
            _tabForeBrush, tabInfo.Bounds, _tabTextFormat);
    }
    else
    {
        gr.FillRectangle(_tabBackBrush, tabInfo.Bounds);
        gr.DrawString(this.TabPages[tabInfo.Index].Text, tabInfo.Font, 
            _tabForeBrush, tabInfo.Bounds, _tabTextFormat);
    }
}

更简单 (IMO):将绘制处理程序添加到 TabPage(不是顶级 TabControl,而是其中的 TabPage),然后以所需的颜色绘制背景矩形。

  1. 在设计器中或"手动"中,将 Paint 事件处理程序添加到 TabPage:

    Page1.Paint += tabpage_Paint; // custom paint event so we get the backcolor we want
    
  2. 在 paint 方法中,将页面矩形绘制为您想要的颜色(就我而言,我希望它遵循标准的 BackColor):

    // force the tab background to the current BackColor
    private void tabpage_Paint(object sender, PaintEventArgs e)
    {
        SolidBrush fillBrush = new SolidBrush(BackColor);
        e.Graphics.FillRectangle(fillBrush, e.ClipRectangle);
    }
    

将面板放在选项卡控件的顶部(而不是内部),并在属性中设置颜色。根据需要调用 panelx.Hide() 和 Panelx.Show()。

遗憾的是,在绘制控件时会处理背景颜色属性。我的建议是执行我所做的操作并创建一个用户控件来模拟选项卡控制器。

我使用菜单条作为选项卡,并将第二个用户控件作为填充停靠到父用户控件。在第二个用户控件中,我能够添加所述选项卡所需的任何内容。

更难的部分是您必须构建所有功能才能使其用作选项卡控件。