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

一个 .NET 平面 TabControl (CustomDraw)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (49投票s)

2005年11月7日

公共领域

3分钟阅读

viewsIcon

455339

downloadIcon

22878

这是一个 CustomDraw TabControl,外观扁平,支持图标,并填充 backcolor 属性。

引言

Visual Studio 中包含的 TabControl 不支持 flat 属性,所以我决定构建自己的控件。 我在互联网上搜索了类似的东西,但找不到任何满足我需求的资源。

好了,这是控件,它看起来是扁平的,支持图标,并用 backcolor 属性填充。

背景

首先,我们需要双缓冲技术来改善绘制并允许控件改变其外观

this.SetStyle(ControlStyles.UserPaint, true);
this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);
this.SetStyle(ControlStyles.DoubleBuffer, true);
this.SetStyle(ControlStyles.ResizeRedraw, true);
this.SetStyle(ControlStyles.SupportsTransparentBackColor, true);

然后,您需要覆盖 OnPaint 事件并绘制您自己的控件。 涉及的基本步骤是

  1. 填充客户端区域。
  2. 绘制边框。
  3. 裁剪用于绘制选项卡的区域,包括 Up-Down 按钮(如果可见)(有关 Up-Down 按钮的子类化,请参见下文)。
  4. 绘制每个选项卡页面。
  5. 通过在边框附近绘制线条来覆盖其他区域(提示!)。
protected override void OnPaint(PaintEventArgs e)
{
  base.OnPaint(e); 
  
  DrawControl(e.Graphics);
}

internal void DrawControl(Graphics g)
{
  if (!Visible)
    return;

  Rectangle TabControlArea = this.ClientRectangle;
  Rectangle TabArea = this.DisplayRectangle;

  //----------------------------
  // fill client area
  Brush br = new SolidBrush(SystemColors.Control);
  g.FillRectangle(br, TabControlArea);
  br.Dispose();
  //----------------------------

  //----------------------------
  // draw border
  int nDelta = SystemInformation.Border3DSize.Width;

  Pen border = new Pen(SystemColors.ControlDark);
  TabArea.Inflate(nDelta, nDelta);
  g.DrawRectangle(border, TabArea);
  border.Dispose();
  //----------------------------


  //----------------------------
  // clip region for drawing tabs
  Region rsaved = g.Clip;
  Rectangle rreg;

  int nWidth = TabArea.Width + nMargin;
  if (bUpDown)
  {
    // exclude updown control for painting
    if (Win32.IsWindowVisible(scUpDown.Handle))
    {
      Rectangle rupdown = new Rectangle();
      Win32.GetWindowRect(scUpDown.Handle, ref rupdown);
      Rectangle rupdown2 = this.RectangleToClient(rupdown);

      nWidth = rupdown2.X;
    }
  }

  rreg = new Rectangle(TabArea.Left, TabControlArea.Top, 
                  nWidth - nMargin, TabControlArea.Height);

  g.SetClip(rreg);

  // draw tabs
  for (int i = 0; i < this.TabCount; i++)
    DrawTab(g, this.TabPages[i], i);

  g.Clip = rsaved;
  //----------------------------


  //----------------------------
  // draw background to cover flat border areas
  if (this.SelectedTab != null)
  {
    TabPage tabPage = this.SelectedTab;
    Color color = tabPage.BackColor;
    border = new Pen(color);
    
    TabArea.Offset(1, 1);
    TabArea.Width -= 2;
    TabArea.Height -= 2;
    
    g.DrawRectangle(border, TabArea);
    TabArea.Width -= 1;
    TabArea.Height -= 1;
    g.DrawRectangle(border, TabArea);

    border.Dispose();
  }
  //----------------------------
}

DrawTab 方法使用多边形绘制边框。 它还绘制选项卡页面的文本和图标。 我只实现了对顶部底部的对齐,因为对左侧和右侧的对齐更加复杂(有人可以尝试!),并且还有其他文章解释了这种行为,例如 .NET 样式的侧面选项卡控件 作者是 helloravi

internal void DrawTab(Graphics g, TabPage tabPage, int nIndex)
{
  Rectangle recBounds = this.GetTabRect(nIndex);
  RectangleF tabTextArea = (RectangleF)this.GetTabRect(nIndex);

  bool bSelected = (this.SelectedIndex == nIndex);

  Point[] pt = new Point[7];
  if (this.Alignment == TabAlignment.Top)
  {
    pt[0] = new Point(recBounds.Left, recBounds.Bottom);
    pt[1] = new Point(recBounds.Left, recBounds.Top + 3);
    pt[2] = new Point(recBounds.Left + 3, recBounds.Top);
    pt[3] = new Point(recBounds.Right - 3, recBounds.Top);
    pt[4] = new Point(recBounds.Right, recBounds.Top + 3);
    pt[5] = new Point(recBounds.Right, recBounds.Bottom);
    pt[6] = new Point(recBounds.Left, recBounds.Bottom);
  }
  else
  {
    pt[0] = new Point(recBounds.Left, recBounds.Top);
    pt[1] = new Point(recBounds.Right, recBounds.Top);
    pt[2] = new Point(recBounds.Right, recBounds.Bottom - 3);
    pt[3] = new Point(recBounds.Right - 3, recBounds.Bottom);
    pt[4] = new Point(recBounds.Left + 3, recBounds.Bottom);
    pt[5] = new Point(recBounds.Left, recBounds.Bottom - 3);
    pt[6] = new Point(recBounds.Left, recBounds.Top);
  }

  //----------------------------
  // fill this tab with background color
  Brush br = new SolidBrush(tabPage.BackColor);
  g.FillPolygon(br, pt);
  br.Dispose();
  //----------------------------

  //----------------------------
  // draw border
  //g.DrawRectangle(SystemPens.ControlDark, recBounds);
  g.DrawPolygon(SystemPens.ControlDark, pt);

  if (bSelected)
  {
    //----------------------------
    // clear bottom lines
    Pen pen = new Pen(tabPage.BackColor);

    switch (this.Alignment)
    {
      case TabAlignment.Top:
        g.DrawLine(pen, recBounds.Left + 1, recBounds.Bottom, 
                        recBounds.Right - 1, recBounds.Bottom);
        g.DrawLine(pen, recBounds.Left + 1, recBounds.Bottom+1, 
                        recBounds.Right - 1, recBounds.Bottom+1);
        break;

      case TabAlignment.Bottom:
        g.DrawLine(pen, recBounds.Left + 1, recBounds.Top, 
                           recBounds.Right - 1, recBounds.Top);
        g.DrawLine(pen, recBounds.Left + 1, recBounds.Top-1, 
                           recBounds.Right - 1, recBounds.Top-1);
        g.DrawLine(pen, recBounds.Left + 1, recBounds.Top-2, 
                           recBounds.Right - 1, recBounds.Top-2);
        break;
    }
    pen.Dispose();
    //----------------------------
  }
  //----------------------------

  //----------------------------
  // draw tab's icon
  if ((tabPage.ImageIndex >= 0) && (ImageList != null) && 
             (ImageList.Images[tabPage.ImageIndex] != null))
  {
    int nLeftMargin = 8;
    int nRightMargin = 2;

    Image img = ImageList.Images[tabPage.ImageIndex];
    
    Rectangle rimage = new Rectangle(recBounds.X + nLeftMargin, 
                        recBounds.Y + 1, img.Width, img.Height);
    
    // adjust rectangles
    float nAdj = (float)(nLeftMargin + img.Width + nRightMargin);

    rimage.Y += (recBounds.Height - img.Height) / 2;
    tabTextArea.X += nAdj;
    tabTextArea.Width -= nAdj;

    // draw icon
    g.DrawImage(img, rimage);
  }
  //----------------------------

  //----------------------------
  // draw string
  StringFormat stringFormat = new StringFormat();
  stringFormat.Alignment = StringAlignment.Center;  
  stringFormat.LineAlignment = StringAlignment.Center;

  br = new SolidBrush(tabPage.ForeColor);

  g.DrawString(tabPage.Text, Font, br, tabTextArea, 
                                       stringFormat);
  //----------------------------
}

为了绘制 UpDown 按钮,我使用了 DrawIcons 方法。 它使用包含四个按钮(左、右、禁用左、禁用右)的 leftRightImages ImageList,并在通过子类化类接收到 WM_PAINT 消息时调用它,如下所述

internal void DrawIcons(Graphics g)
{
  if ((leftRightImages == null) || 
            (leftRightImages.Images.Count != 4))
    return;

  //----------------------------
  // calc positions
  Rectangle TabControlArea = this.ClientRectangle;

  Rectangle r0 = new Rectangle();
  Win32.GetClientRect(scUpDown.Handle, ref r0);

  Brush br = new SolidBrush(SystemColors.Control);
  g.FillRectangle(br, r0);
  br.Dispose();
  
  Pen border = new Pen(SystemColors.ControlDark);
  Rectangle rborder = r0;
  rborder.Inflate(-1, -1);
  g.DrawRectangle(border, rborder);
  border.Dispose();

  int nMiddle = (r0.Width / 2);
  int nTop = (r0.Height - 16) / 2;
  int nLeft = (nMiddle - 16) / 2;

  Rectangle r1 = new Rectangle(nLeft, nTop, 16, 16);
  Rectangle r2 = new Rectangle(nMiddle+nLeft, nTop, 16, 16);
  //----------------------------

  //----------------------------
  // draw buttons
  Image img = leftRightImages.Images[1];
  if (img != null)
  {
    if (this.TabCount > 0)
    {
      Rectangle r3 = this.GetTabRect(0);
      if (r3.Left < TabControlArea.Left)
        g.DrawImage(img, r1);
      else
      {
        img = leftRightImages.Images[3];
        if (img != null)
          g.DrawImage(img, r1);
      }
    }
  }

  img = leftRightImages.Images[0];
  if (img != null)
  {
    if (this.TabCount > 0)
    {
      Rectangle r3 = this.GetTabRect(this.TabCount - 1);
      if (r3.Right > (TabControlArea.Width - r0.Width))
        g.DrawImage(img, r2);
      else
      {
        img = leftRightImages.Images[2];
        if (img != null)
          g.DrawImage(img, r2);
      }
    }
  }
  //----------------------------
}

关注点

好了,这是绘制 UpDown 按钮的技巧(这是最困难的任务)。

首先,我需要知道何时应该绘制它们。 可以通过处理三个事件来实现:OnCreateControlControlAddedControlRemoved

protected override void OnCreateControl()
{
  base.OnCreateControl();

  FindUpDown();
}

private void FlatTabControl_ControlAdded(object sender, 
                                      ControlEventArgs e)
{
  FindUpDown();
  UpdateUpDown();
}

private void FlatTabControl_ControlRemoved(object sender, 
                                       ControlEventArgs e)
{
  FindUpDown();
  UpdateUpDown();
}

函数 FindUpDown 通过使用 Win32 GetWindow 并查找 TabControl 的子窗口来查找类 msctls_updown32(来自 Oleg Lobach完全所有者绘制的选项卡控件 的一个了不起的提示)

如果我们找到该类,我们可以对其进行子类化以处理消息 WM_PAINT(有关子类化的更多信息,请参阅 Sameers.NET 中的子类化 - 纯 .NET 方法Tomas Brennan黑掉组合框使其具有水平滚动条)。

private void FindUpDown()
{
  bool bFound = false;

  // find the UpDown control
  IntPtr pWnd = 
      Win32.GetWindow(this.Handle, Win32.GW_CHILD);
  
  while (pWnd != IntPtr.Zero)
  {
    //----------------------------
    // Get the window class name
    char[] className = new char[33];

    int length = Win32.GetClassName(pWnd, className, 32);

    string s = new string(className, 0, length);
    //----------------------------

    if (s == "msctls_updown32")
    {
      bFound = true;

      if (!bUpDown)
      {
        //----------------------------
        // Subclass it
        this.scUpDown = new SubClass(pWnd, true);
        this.scUpDown.SubClassedWndProc += 
            new SubClass.SubClassWndProcEventHandler(
                                 scUpDown_SubClassedWndProc);
        //----------------------------

        bUpDown = true;
      }
      break;
    }
    
    pWnd = Win32.GetWindow(pWnd, Win32.GW_HWNDNEXT);
  }

  if ((!bFound) && (bUpDown))
    bUpDown = false;
}

private void UpdateUpDown()
{
  if (bUpDown)
  {
    if (Win32.IsWindowVisible(scUpDown.Handle))
    {
      Rectangle rect = new Rectangle();

      Win32.GetClientRect(scUpDown.Handle, ref rect);
      Win32.InvalidateRect(scUpDown.Handle, ref rect, true);
    }
  }
}

这是 WndProc 的子类化函数。 我们处理 WM_PAINT 以绘制图标并验证客户端区域

private int scUpDown_SubClassedWndProc(ref Message m) 
{
  switch (m.Msg)
  {
    case Win32.WM_PAINT:
    {
      //------------------------
      // redraw
      IntPtr hDC = Win32.GetWindowDC(scUpDown.Handle);
      Graphics g = Graphics.FromHdc(hDC);

      DrawIcons(g);

      g.Dispose();
      Win32.ReleaseDC(scUpDown.Handle, hDC);
      //------------------------

      // return 0 (processed)
      m.Result = IntPtr.Zero;

      //------------------------
      // validate current rect
      Rectangle rect = new Rectangle();

      Win32.GetClientRect(scUpDown.Handle, ref rect);
      Win32.ValidateRect(scUpDown.Handle, ref rect);
      //------------------------
    }
    return 1;
  }

  return 0;
}

使用代码

要使用该代码,只需添加对 FlatTabControl 的引用,并将普通的 TabControl 更改为 FlatTabControl。 所有属性保持不变。 您可以尝试使用 backcolor 属性和图标来获得您想要的外观

/// <SUMMARY>
/// Summary description for Form1.
/// </SUMMARY>
public class Form1 : System.Windows.Forms.Form
{
  private FlatTabControl.FlatTabControl tabControl1;
  
  ...
    
  #region Windows Form Designer generated code
  /// <SUMMARY>
  /// Required method for Designer support - do not modify
  /// the contents of this method with the code editor.
  /// </SUMMARY>
  private void InitializeComponent()
  {
  
  ...
    
    this.tabControl1 = new FlatTabControl.FlatTabControl();
  ...
    
  }
  #endregion

参考和致谢

请参阅以下有用资源

历史

  • 2005 年 11 月 7 日:1.0 版本
  • 2005 年 12 月 5 日:1.1 版本
    • 添加了 myBackColor 属性。

注意

请发表您的评论、更正或学分要求。 欢迎您的反馈.

© . All rights reserved.