TabControl & custom classes

dabossss

Member
Joined
Jan 12, 2007
Messages
14
Programming Experience
1-3
Hi guys,
I'm trying to implement a TabPage that also has a checkbox displayed with the text (and hence, a boolean state).
I've set the TabControl to OwnerDrawFixed, and managed to display the checkbox using CheckboxRenderer.
I've also
made a custom TabPage class that has an IsChecked property. Now my problem is, how do I get the customized TabControl to work with the customized TabPages? I can display the custom TabPages alright by the following call:

CheckedTabControl1.Controls.Add(New TabPage("Custom Tab"))

(my custom class is also called TabPage)
This displays it, but I can't access the IsChecked property from within the CustomTabControl class, as it says that "'IsChecked' is not a member of 'System.Windows.Forms.TabPage'."
How do I set the CustomTabControl class to consist of my custom TabPages?

Any help would greatly be appreciated.

Cheers,

dabossss
 
Yeah thats what I was thinking with the Invalidate. I can't tell the difference though. Maybe once I start adding more tabs. Anyway, its still the way to go.

I've changed the CustomTabControl's SizeMode to Fixed, and for some reason that seems to make it all better (although the tabs are slightly too big now, its definitely better than before). Will I run into problems once I start adding a lot more tabs?

Here's a snippet of the code I have for the Selecting bit:
VB.NET:
Public Class CheckedTabControl
    Inherits TabControl
    Dim CheckSize As Size
    Dim IgnoreClick As Boolean

    Public Sub New()
        Me.DrawMode = TabDrawMode.OwnerDrawFixed
        Me.SizeMode = TabSizeMode.Fixed
        AddHandler Me.Selecting, AddressOf SelectingHandler
    End Sub
    Protected Sub SelectingHandler(ByVal sender As Object, ByVal e As System.Windows.Forms.TabControlCancelEventArgs)
        e.Cancel = IgnoreClick
    End Sub
    Protected Overrides Sub OnMouseDown(ByVal e As System.Windows.Forms.MouseEventArgs)
        For i As Integer = 0 To Me.TabCount - 1
            Dim tabRect As Rectangle = Me.GetTabRect(i)
            Dim checkRect As New Rectangle(tabRect.Location, CheckSize)
            checkRect.Offset(Me.TabPages(i).Margin.Left, (Me.TabPages(i).Margin.Top + Me.TabPages(i).Margin.Bottom) / 2)
            If (DirectCast(Me.TabPages(i), CheckedTabPage).IsCheckedType() And checkRect.Contains(e.Location)) Then
                IgnoreClick = True
                DirectCast(Me.TabPages(i), CheckedTabPage).ToggleCheck()
                Me.Invalidate(checkRect)
            Else
                IgnoreClick = False
            End If
        Next i
    End Sub
Why doesn't that work? I've even put the IgnoreClick=False assignment before I toggle or Invalidate in case it interrupts... Its an OnMouseDown, so it should be before the Selecting gets called... What's the problem?
 
Have you set the 'CheckSize' anywhere else, or is it size '0' ?

A tip: It is also faster to just select the events from the class event list to have the handler method generated (the usual '..Handles' stuff) than writing the OnEvent yourself or writing event handler signature and use addhandler. This is the same for inherited classes as subscribing events for regular class instances.
 
CheckSize is set in OnDrawItem. Presumably this would be executed before any MouseDown events, etc?
Thanks for the handler tip. I couldn't see it before, so I assumed i had to override.
The reason the Selecting thing wasn't working was because of the loop in the OnMouseDown. The boolean was being set back to false before the Selecting event was called.
I've fixed this by setting the boolean to false at the very start of the event, and then not having the "else" case. It *mostly* works (except the start case), but now for some reason it takes 2 clicks on a tab to load it! Any reason why? I've even set the boolean back to false and the end of the Selecting event just in case... That still doesn't help though.
VB.NET:
Public Class CheckedTabControl
    Inherits TabControl
    Dim CheckSize As Size
    Dim IgnoreClick As Boolean = False
    Public Sub New()
        Me.DrawMode = TabDrawMode.OwnerDrawFixed
        Me.SizeMode = TabSizeMode.Fixed
    End Sub

    Private Sub CheckedTabControl_Selecting(ByVal sender As Object, ByVal e As System.Windows.Forms.TabControlCancelEventArgs) Handles Me.Selecting
        e.Cancel = IgnoreClick
        IgnoreClick = False
    End Sub

    Protected Overrides Sub OnMouseDown(ByVal e As System.Windows.Forms.MouseEventArgs)
        IgnoreClick = False
        For i As Integer = 0 To Me.TabCount - 1
            Dim tabRect As Rectangle = Me.GetTabRect(i)
            Dim checkRect As New Rectangle(tabRect.Location, CheckSize)
            checkRect.Offset(Me.TabPages(i).Margin.Left, (Me.TabPages(i).Margin.Top + Me.TabPages(i).Margin.Bottom) / 2)
            If (DirectCast(Me.TabPages(i), CheckedTabPage).IsCheckedType() And checkRect.Contains(e.Location)) Then
                IgnoreClick = True
                DirectCast(Me.TabPages(i), CheckedTabPage).ToggleCheck()
                'MsgBox(Me.TabPages(i).Text & " is " & DirectCast(Me.TabPages(i), CheckedTabPage).IsChecked())
                Me.Invalidate(checkRect)
            End If
        Next i
    End Sub

    Protected Overrides Sub OnDrawItem(ByVal e As DrawItemEventArgs)
        Dim g As Graphics = e.Graphics
        Dim pageNum As Integer = e.Index
        Dim _TabPage As CheckedTabPage = DirectCast(Me.TabPages(pageNum), CheckedTabPage)
        Dim _TabBounds As Rectangle = Me.GetTabRect(pageNum)
        Dim _TabFont As New Font("Microsoft Sans Serif", 8, FontStyle.Regular)
        _TabPage.Width += CheckBoxRenderer.GetGlyphSize(g, CheckBoxState.CheckedNormal).Width
        Dim StringFlags As New StringFormat()
        StringFlags.Alignment = StringAlignment.Center
        StringFlags.LineAlignment = StringAlignment.Center
        If _TabPage.IsCheckedType() Then
            'StringFlags.Alignment = StringAlignment.Far
            CheckSize = CheckBoxRenderer.GetGlyphSize(g, CheckBoxState.CheckedNormal)
            Dim CheckLoc = _TabBounds.Location
            CheckLoc.Offset(_TabPage.Margin.Left, (_TabPage.Margin.Top + _TabPage.Margin.Bottom) / 2)
            If _TabPage.IsChecked Then
                CheckBoxRenderer.DrawCheckBox(g, CheckLoc, CheckBoxState.CheckedNormal)
            Else
                CheckBoxRenderer.DrawCheckBox(g, CheckLoc, CheckBoxState.UncheckedNormal)
            End If
            _TabBounds.Width += CheckSize.Width
        End If
        g.DrawString(_TabPage.Text, _TabFont, Brushes.Black, _TabBounds, New StringFormat(StringFlags))
    End Sub
End Class
 
Last edited:
I noticed the same 'two-click' misbehaviour to select tab when testing quickly earlier, but haven't had time to look into it yet. It may have something to do with interupting the normal mouse down-click-up sequence for the internal tabcontrol code when you cancel the tab Selecting after mousedown. Perhaps something with tabcontrol/tabpage losing focus.
 
Genious!! How come I didn't check the order of Selecting event with the mouse ones..? (because normally it is the MouseClick that triggers the action) I did check now, and guess what, Selecting actually happens before any of the mouse events, aint that peculiar! Here is the event order I recorded:
:Selecting::MouseDown::MouseClick::MouseUp:
Given this, you can drop the MouseDown pre-check and just do the Selecting event, something like this:
VB.NET:
Dim pt As Point = Me.PointToClient(System.Windows.Forms.Cursor.Position)
Dim tabRect As Rectangle = Me.GetTabRect(e.TabPageIndex)
Dim checkRect As New Rectangle(tabRect.Location, CheckSize)
If checkRect.Contains(pt) Then e.Cancel = True
As it turned out, the IgnoreClick was chasing the next round of events, not the current. Talk about Back to the Future deja-vu feeling..
 
Since Selecting is the first in order, I've changed the MouseDown to MouseClicked.
There's still a bit of weird behaviour when dealing with multiline tabcontrols, but I'm at the stage where I don't care anymore ;)

Thanks heaps for all your help! You're a legend!
 
I have investigated the multiline tabs also. I found a solution, but it is a bit hacky.

First a little explanation: I label the tab lines from closest to the visible page "level 1", 2,3 etc. upwards. When clicking a tab at another line than level 1, the tab line is moved down now to be level 1. This happens pre-Selecting event. The reported GetTabRect is valid after the move has happened in Selecting event, but the problem is how to detect the mouse location (and possibly the checkbox rectangle) when the target has already relocated.

You can still use the X coordinate of the mouse found (my previous post pt variable), the tabs don't relocate on the X-axis in multiline mode. Since you have got the correct tab rectangle and know where in that rectangle the checkbox is, you can calculate a midpoint for Y coordinate according to this. (the checkbox fills most of the tab rectangles height anyway).

What happens when a multiline tab of a higher level is successfully detected and cancelled is that that line of tabs is moved back up, but only one level.. ie if it was line level 3 it moved immediately down to level 1 where it was cancelled and moved up to level 2. (sounds crazy, I know, you got to see it happen). The same hacky behaviour goes for a 2 line multiline setup, but it looks more "predictable" when the cancelled level 2 line always return back to level 2.

Here is the modified code:
VB.NET:
Private Sub CheckedTabControl_Selecting(ByVal sender As Object, ByVal e As System.Windows.Forms.TabControlCancelEventArgs) _
Handles Me.Selecting
    doToggle = False
    Dim pt As Point = Me.PointToClient(System.Windows.Forms.Cursor.Position)
    Dim tabRect As Rectangle = Me.GetTabRect(e.TabPageIndex)
    Dim adjusted As Point = New Point(pt.X, tabRect.Location.Y + (tabRect.Height \ 2))
    Dim checkRect As New Rectangle(tabRect.Location, CheckSize)
    If checkRect.Contains(adjusted) Then
        e.Cancel = True
        'DirectCast(e.TabPage, CheckedTabPage).ToggleCheck()
        doToggle = True
        tabToToggle = e.TabPage
    End If
End Sub
 
Private doToggle As Boolean
Private tabToToggle As CheckedTabPage
 
Private Sub CheckedTabControl_MouseClick(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) _
Handles Me.MouseClick
    If doToggle Then
        doToggle = False
        tabToToggle.ToggleCheck()
    Else
        Dim tabRect As Rectangle = Me.GetTabRect(Me.SelectedIndex)
        Dim checkRect As New Rectangle(tabRect.Location, CheckSize)
        If checkRect.Contains(e.Location) Then
            DirectCast(Me.SelectedTab, CheckedTabPage).ToggleCheck()
        End If
    End If
End Sub
Because of how bad I think this looks with the tab lines relocation, I wouldn't consider this anything else than a proof of concept, and not something I would like to see in a application if I could avoid it.

There is one more note, about single line tabs that extend beyond visibility, the last partial visible tab does also relocate into view when clicked and consequently does not respond to the checkbox behaviour until that tab is fully in view. I haven't bothered looking into calculating this.

Edit: I just noticed another one... Selecting doesn't happen for a tab already selected. The necessary code is added to MouseClick to handle this case.
 
Last edited:
We think identically!
I was thinking of exactly the same thing for the multiline - I didn't bother implementing it because I thought it was very dodgy. Are there any events that would get triggered before the line-move thing?

I also noticed the single line visibility check bug. I was just about to start fixing it, but I'm pretty sure its going to be the same issue as the multi-line - ie. pre-Selecting action.

What did you mean by Selecting not triggering for a selected tab? It shouldn't need to select it! And checking the checkbox *does* work for the currently selected tab...
 
Are there any events that would get triggered before the line-move thing?
Did you figure this out also? *lol* :)
Actually there is the Deselecting that happens to the tab losing selection before the Selecting happen to the one getting it.. So the logic is easy knowing this; don't allow any tab to deselect unless the mouseclick has done its job and possibly temporarily allows and enforces the selection. This works perfectly! Makes the previous hack look like a bad poltergeist. The code looks as follows:
VB.NET:
Private Sub CheckedTabControl_Deselecting(ByVal sender As Object, ByVal e As System.Windows.Forms.TabControlCancelEventArgs) _
Handles Me.Deselecting
    e.Cancel = Not allowDeselect
    allowDeselect = False
End Sub
 
Private allowDeselect As Boolean '=False by default
 
Private Sub CheckedTabControl_MouseClick(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) _
Handles Me.MouseClick
    For i As Integer = 0 To Me.TabCount - 1
        Dim tabRect As Rectangle = Me.GetTabRect(i)
        If tabRect.Contains(e.Location) Then
            Dim checkRect As New Rectangle(tabRect.Location, CheckSize)
            If checkRect.Contains(e.Location) Then
                DirectCast(Me.TabPages(i), CheckedTabPage).ToggleCheck()
            Else
                allowDeselect = True
                Me.SelectedTab = Me.TabPages(i)
            End If
            Exit For
        End If
    Next i
End Sub
(works for the partially visible single-line tabs also!)

In case you missed an earlier comment, here is what I do in the CheckedTabpage IsChecked property setter for Invalidate instead of Refresh, this avoids any flicker and unnecessary paint processing for the rest of the control:
VB.NET:
Set(ByVal value As Boolean)
    checked = value
    If Me.Parent IsNot Nothing Then
        Dim tc As TabControl = DirectCast(Me.Parent, TabControl)
        Dim tabRect As Rectangle = tc.GetTabRect(tc.TabPages.IndexOf(Me))
        Dim checkRect As New Rectangle(tabRect.Location, New Size(13, 13))
        tc.Invalidate(checkRect)
    End If
End Set
 
Back
Top