Tabpage header stays below arrow buttons after moving

Viewed 617

The situation is as follows, I have a custom TabControl that paints the TabHeaders itself so I can choose colors etc.

The problem occurs when I have more TabHeaders then the TabControl can fit and the arrows (spin control) appears. Whenever I then cycle all the way to the right (so there are no more headers to the right) the tabheader that was behind the arrows is still partially there.

Image to demonstrate:

enter image description here

What I know:
If I manually call .Invalidate() so it repaints the problem will go away, however I can't find an event that is triggered by the arrow keys (it does not have a scroll event).

Events that won't work:

  • Click
  • Layout
  • Resize (don't even ask)

I had WndProc in mind (it has cases with get 'spin' which sounds like the spincontrol)


Custom tab control code:

class ColoredTabControl : TabControl
{
    public Color ActiveTabColor
    {
        get
        {
            return _activeTabColor.Color;
        }
        set
        {
            _activeTabColor.Color = value;
            this.Invalidate();
        }
    }
    public Color DefaultTabColor
    {
        get
        {
            return _defaultTabColor.Color;
        }
        set
        {
            _defaultTabColor.Color = value;
            this.Invalidate();
        }
    }

    private SolidBrush _activeTabColor = new SolidBrush( Color.LightSteelBlue );
    private SolidBrush _defaultTabColor = new SolidBrush( Color.LightGray );

    private int _biggestTextHeight = 0;
    private int _biggestTextWidth = 0;

    public ColoredTabControl()
    {
        this.SetStyle( ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true );
    }

    protected override void OnPaint( PaintEventArgs e )
    {
        base.OnPaint( e );
        var graphics = e.Graphics;

        TabPage currentTab = this.SelectedTab;
        if( TabPages.Count > 0 )
        {
            var biggestTextHeight = TabPages?.Cast<TabPage>()?.Max( r => TextRenderer.MeasureText( r.Text, r.Font ).Height );
            var biggestTextWidth = TabPages?.Cast<TabPage>()?.Max( r => TextRenderer.MeasureText( r.Text, r.Font ).Width );
            if( biggestTextHeight > _biggestTextHeight || biggestTextWidth > _biggestTextWidth )
            {
                _biggestTextWidth = ( int ) biggestTextWidth;
                _biggestTextHeight = ( int ) biggestTextHeight;
                this.ItemSize = new Size( _biggestTextWidth + 5, _biggestTextHeight + 10 );
            }
        }
        for( int i = 0; i < TabPages.Count; i++ )
        {
            TabPage tabPage = TabPages[ i ];
            Rectangle tabRectangle = GetTabRect( i );
            SolidBrush brush = ( tabPage == currentTab ? _activeTabColor : _defaultTabColor );

            graphics.FillRectangle( brush, tabRectangle );

            TextRenderer.DrawText( graphics, tabPage.Text, tabPage.Font, tabRectangle, tabPage.ForeColor );
        }
    }

    protected override void OnPaintBackground( PaintEventArgs e )
    {
        base.OnPaintBackground( e );
    }
}

A bit of form code to repro the issue:

var tabControl = new ColoredTabControl() { Width = 340, Height = 300, Top = 100 };
tabControl.TabPages.Add( "text" );
tabControl.TabPages.Add( "another tab");
tabControl.TabPages.Add( "yet another" );
tabControl.TabPages.Add( "another one" );
tabControl.TabPages.Add( "another" );
tabControl.DefaultTabColor = Color.Red; //To see the issue clearly
Controls.Add( tabControl );
2 Answers

I ended up fixing it by hooking into WndProc event handler of the msctls_updown32 class. I did this by using a Win32 class I got from a codeproject tabcontrol project by Oscar Londono I had to adjust it a bit for my needs though.

Added code in the tabcontrol:

protected override void OnSelectedIndexChanged( EventArgs e )
{
    base.OnSelectedIndexChanged( e );
    this.Invalidate();
}

protected override void OnControlAdded( ControlEventArgs e )
{
    base.OnControlAdded( e );
    FindUpDown();
}

protected override void Dispose( bool disposing )
{
    if(scUpDown != null)
    {
        scUpDown.SubClassedWndProc -= scUpDown_SubClassedWndProc;
    }
    base.Dispose( disposing );
}

bool bUpDown = false;
SubClass scUpDown = null;

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[] fullName = new char[ 33 ];

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

        string className = new string( fullName, 0, length );

        if( className == Win32.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 int scUpDown_SubClassedWndProc( ref Message m )
{
    switch(m.Msg)
    {
        case Win32.WM_LCLICK:
            //Invalidate to repaint
            this.Invalidate();
            return 0;
    }
    return 0;
}

The other part of this class is still 100% the same as in the OP

Win32 class:

internal class Win32
{
    /*
     * GetWindow() Constants
     */
    public const int GW_HWNDFIRST           = 0;
    public const int GW_HWNDLAST            = 1;
    public const int GW_HWNDNEXT            = 2;
    public const int GW_HWNDPREV            = 3;
    public const int GW_OWNER               = 4;
    public const int GW_CHILD               = 5;

    public const int WM_NCCALCSIZE          = 0x83;
    public const int WM_WINDOWPOSCHANGING   = 0x46;
    public const int WM_PAINT               = 0xF;
    public const int WM_CREATE              = 0x1;
    public const int WM_NCCREATE            = 0x81;
    public const int WM_NCPAINT             = 0x85;
    public const int WM_PRINT               = 0x317;
    public const int WM_DESTROY             = 0x2;
    public const int WM_SHOWWINDOW          = 0x18;
    public const int WM_SHARED_MENU         = 0x1E2;
    public const int WM_LCLICK              = 0x201;
    public const int HC_ACTION              = 0;
    public const int WH_CALLWNDPROC         = 4;
    public const int GWL_WNDPROC            = -4;

    //Class name constant
    public const string MSCTLS_UPDOWN32 = "msctls_updown32";

    [ DllImport("User32.dll",CharSet = CharSet.Auto)]
    public static extern int GetClassName(IntPtr hwnd, char[] className, int maxCount);

    [DllImport("User32.dll",CharSet = CharSet.Auto)]
    public static extern IntPtr GetWindow(IntPtr hwnd, int uCmd);

}

#region SubClass Classing Handler Class
internal class SubClass : System.Windows.Forms.NativeWindow
{
    public delegate int SubClassWndProcEventHandler(ref System.Windows.Forms.Message m);
    public event SubClassWndProcEventHandler SubClassedWndProc;
    private bool IsSubClassed = false;

    public SubClass(IntPtr Handle, bool _SubClass)
    {
        base.AssignHandle(Handle);
        this.IsSubClassed = _SubClass;
    }

    public bool SubClassed
    {
        get{ return this.IsSubClassed; }
        set{ this.IsSubClassed = value; }
    }

    protected override void WndProc(ref Message m) 
    {
        if (this.IsSubClassed)
        {
            if (OnSubClassedWndProc(ref m) != 0)
                return;
        }
        base.WndProc(ref m);
    }

    private int OnSubClassedWndProc(ref Message m)
    {
        if (SubClassedWndProc != null)
        {
            return this.SubClassedWndProc(ref m);
        }

        return 0;
    }
}
#endregion

If anyone has a better answer or a question feel free to answer/comment!

If anyone has a way or even thoughts on how to hook into the actual scroll event of the tabcontrol please answer/comment.

If you are curious to know how to get the scroll event of the tab header when you click on those scroll buttons, you can handle WM_HSCROLL event of the TabControl:

const int WM_HSCROLL = 0x0114;
protected override void WndProc(ref Message m)
{
    base.WndProc(ref m);
    if (m.Msg == WM_HSCROLL)
        this.Invalidate();
} 

This way you can invalidate the control after scrolling using those buttons.

Also for the cases that user changed selected tab using keyboard, you can override OnSelectedIndexChanged and after calling base method, invalidate the control.

Related