Tabpage header 移动后停留在箭头按钮下方

Tabpage header stays below arrow buttons after moving

情况如下,我有一个自定义的TabControlTabHeaders本身,所以我可以选择颜色等

当我有更多 TabHeaders 然后 TabControl 可以容纳并且箭头 (spin control) 出现时,问题就出现了。每当我然后一直循环到右边(所以右边不再有 headers)时,箭头后面的制表符仍然部分存在。

图片演示:

我知道的:
如果我手动调用 .Invalidate() 以便重新绘制,问题就会消失,但是我找不到由箭头键触发的事件(它没有滚动事件)。

无效事件:

I had WndProc in mind(它有 get 'spin' 的情况,听起来像自旋控制)


自定义选项卡控件代码:

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 );
    }
}

重现问题的一些表单代码:

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 );

我最终通过挂接到 msctls_updown32 class 的 WndProc 事件处理程序来修复它。我是通过使用从 a codeproject tabcontrol project by Oscar Londono 获得的 Win32 class 来完成此操作的,不过我不得不根据自己的需要对其进行一些调整。

在选项卡控件中添加代码:

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;
}

此 class 的其他部分仍然与 OP

中的 100% 相同

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

如果有人有更好的答案或问题,请随时 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.

如果您想知道如何在单击那些滚动按钮时获取选项卡 header 的滚动事件,您可以处理 TabControlWM_HSCROLL 事件:

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

这样您就可以在使用这些按钮滚动后使控件无效。

对于用户使用键盘更改所选选项卡的情况,您可以重写 OnSelectedIndexChanged 并在调用基本方法后使控件无效。