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
 
Cast the tabpage object to its strong type. Your own TabPage class has a different namespace than the 'System.Windows.Forms.TabPage', check the project properties it belongs to, in Application tab is says Root Namespace. For example if it's inside a 'WindowsApplication1' project the namespace is likely the same, thus 'WindowsApplication1.TabPage', so the casting code will be:
VB.NET:
Dim myTP As WindowsApplication1.TabPage = DirectCast(Tabcontrol1.SelectedTab, WindowsApplication1.TabPage)
If the class is part of current project, the local class definition it will take precedence over an imported (Windows.Forms) and you actually don't need to qualify the namespace, this is called 'narrowest scope' - so this code works too:
VB.NET:
Dim myTP As TabPage = DirectCast(Tabcontrol1.SelectedTab, TabPage)
I question the intentional use of same class name as a standard Framework control, makes it more difficult and confusing for yourself or anyone when using the class. Even when supplying it through a Class Library with apparent namespace difference the problem is as much a nuicance because since user will always need Windows.Forms namespace and import this and then get lots ambiguous problems and perhaps have to use namespace aliases. Frameworks naming guidelines for classes recommend using the type name inherited from as a suffix, example 'CheckedTabPage'.
 
At Dim _TabPage As CheckedTabPage = DirectCast(Me.SelectedTab, CheckedTabPage)
I get:
System.InvalidCastException was unhandled
Message="Unable to cast object of type 'System.Windows.Forms.TabPage' to type 'WindowsApplication1.CheckedTabPage'."

This is the same error I got when trying to CType the standard tab to my custom one. I don't really understand why though - the only real difference between the two is the boolean value, and that is given a default value at creation (so it should always have a value).
I tried implementing a CType widening operator in the CheckedTabPage class (I've renamed it now as you suggested), but VB doesn't allow CType to be defined from a base class to its subclasses.

Any suggestions? This really has me stumped!
 
Works just fine if the tabpage really is of type CheckedTabPage. Are you sure there isn't any other regular TabPage controls in the TabControl. You can check the type before casting with the TypeOf operator. (if typeof tp is checkedtabpage then.. you can cast)
 
Grrrr! I'm an absolute idiot! I was creating a TabPage instead of a CustomTabPage in Form1_Load ever since I rename the (custom) TabPage class! Thanks JohnH!

Here's the code I have at the moment:

VB.NET:
Imports System
Imports System.Drawing
Imports System.Windows.Forms
Imports System.Windows.Forms.VisualStyles
Public Class Form1
    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        Dim customTab1 As New CheckedTabPage("Custom Tab")
        Dim customTab2 As New CheckedTabPage("Another Tab")
        CheckedTabControl1.Controls.Add(customTab1)
        CheckedTabControl1.Controls.Add(customTab2)
        AddHandler customTab1.MouseDoubleClick, AddressOf HandleMouseDoubleClick
        AddHandler customTab2.MouseDoubleClick, AddressOf HandleMouseDoubleClick
    End Sub
    Private Sub HandleMouseDoubleClick(ByVal sender As System.Object, ByVal e As System.Windows.Forms.MouseEventArgs)
        sender.ToggleCheck()
        MsgBox(sender.Name & " checked: " & sender.IsChecked())
    End Sub
End Class

Public Class CheckedTabControl
    Inherits TabControl
    Public Sub New()
        Me.DrawMode = TabDrawMode.OwnerDrawFixed
    End Sub
    Protected Overrides Sub OnMouseClick(ByVal e As System.Windows.Forms.MouseEventArgs)
        'MsgBox(Me.GetChildAtPoint(e.Location).ToString)
        'MyBase.OnMouseClick(e)
        For i As Integer = 0 To Me.TabCount - 1
            If Me.GetTabRect(i).Contains(e.Location) Then
                'MsgBox(Me.TabPages(i).Text & " is " & Me.TabPages(i).IsChecked())
            End If
        Next
    End Sub
    Protected Overrides Sub OnDrawItem(ByVal e As DrawItemEventArgs)
        Dim g As Graphics = e.Graphics
        Dim pageNum As Integer = e.Index
        ' Get the item from the collection.
        'Dim _TabPage As CheckedTabPage = DirectCast(Me.SelectedTab, CheckedTabPage)
        Dim _TabPage As CheckedTabPage = DirectCast(Me.TabPages(pageNum), CheckedTabPage)

        ' Get the real bounds for the tab rectangle.
        Dim _TabBounds As Rectangle = Me.GetTabRect(pageNum)

        ' Use our own font.
        Dim _TabFont As New Font("Microsoft Sans Serif", 8, FontStyle.Regular)

        ' Draw string. Center the text.
        Dim StringFlags As New StringFormat()
        StringFlags.Alignment = StringAlignment.Center
        StringFlags.LineAlignment = StringAlignment.Center
        If Not _TabPage.IsChecked Then
            CheckBoxRenderer.DrawCheckBox(g, _TabBounds.Location, CheckBoxState.CheckedNormal)
        Else
            CheckBoxRenderer.DrawCheckBox(g, _TabBounds.Location, CheckBoxState.UncheckedNormal)
        End If

        '_TabBounds.X += CheckBoxRenderer.GetGlyphSize(g, CheckBoxState.CheckedHot).Width
        _TabBounds.Width += CheckBoxRenderer.GetGlyphSize(g, CheckBoxState.CheckedHot).Width + 5
        g.DrawString(_TabPage.Text, _TabFont, Brushes.Black, _TabBounds, New StringFormat(StringFlags))
    End Sub

End Class

Public Class CheckedTabPage
    Inherits System.Windows.Forms.TabPage
    Private checked As Boolean = False
    Sub New()
        MyBase.New()
        checked = False
    End Sub
    Sub New(ByVal text As String)
        MyBase.New()
        checked = False
        Me.Text = text
    End Sub
    Public Sub ToggleCheck()
        If IsChecked() Then
            IsChecked = False
        Else
            IsChecked = True
        End If
    End Sub
    Public Property IsChecked() As Boolean
        Get
            IsChecked = checked
        End Get
        Set(ByVal value As Boolean)
            checked = value
        End Set
    End Property
End Class
Now I want to try to get an Event Handler to change the status of the checkbox (and hence redraw it). I've tried MouseDoubleClick, but it doesn't seem to work. I tried using one of the CustomTabControl events, but because TabControls consist of standard TabPages, I get an error saying that IsChecked() is not a member of System.Windows.Forms.TabPage
Thats a slight problem. Also, I just realised that every time I cast a TabPage into a CustomTabPage (which is every time the CustomTabControl gets redrawn), a new object is essentially being created, so the boolean state gets reset?

Am I going about this whole thing the wrong way? It seemed to be the right way when I started... I can't think of any other way to do it, but the current method seems to have more holes than swiss cheese.
 
You're not creating a new object when casting, you're simply giving the compiler a different perspective on this object reference.

You can make the checkbox much easier if you want:
VB.NET:
Public Class CheckedTabPage
    Inherits TabPage
 
    Private cb As New CheckBox
 
    Public Sub New()
        'set cb location/text other poperties if you want
        Me.Controls.Add(cb)
    End Sub
 
    Public ReadOnly Property IsChecked() As Boolean
        Get
            Return cb.Checked
        End Get
    End Property
 
End Class
 
Thats great to know! In fact, that sort-of fixes most of my problems!
Using a checkbox instead of a boolean does simplify things a little bit (I am mimicking a checkbox anyway!), and the "IsChecked() is not a member..." problem is fixed when I DirectCast it and use IsChecked on that... Its an inconvenience having to cast it every time, but only a minor one!

The events seem to be working too (I'm now working with the CustomTabControl rather than CustomTabPage because I only want to respond to events on the top of the tab). Now all I have to do is sort the formatting out (at the moment rendering the checkbox makes the tab text overflow), and also to only respond to events on the checkbox itself rather than the entire tab.

Thanks SOOOOOOO much for your help! You are an absolute life-saver!!!
 
If you want a TabControl that only works with CheckedTabPages it can be done by inheriting the regular TabControl and the TabControlCollection, but it's some work..

The checkbox can be declared WithEvents, this enables you to select it from control list and just choose its events list "as usual".
 
Here's the code I have in the CheckedTabControl class. For some reason it doesn't seem to respond (I'm trying to get it to respond to double-clicks in the checkbox area, which is 13 x 13 pixels). I know the event is triggered because a simple trace message amongst the code gets triggered. I though the problem might be that the TabCount might be 0 because of the custom TabClass, but again, a trace message confirms that everything is fine there. Any ideas?
VB.NET:
    Protected Overrides Sub OnMouseDoubleClick(ByVal e As System.Windows.Forms.MouseEventArgs)
        For i As Integer = 0 To Me.TabCount - 1
            Dim rect As New Rectangle(Me.TabPages(i).Left, Me.TabPages(i).Top, 13, 13)
            If rect.Contains(e.Location) Then
                MsgBox("box clicked")
            End If
        Next i
    End Sub
 
The checkbox already got a DoubleClick event, why don't you handle this?
 
I omitted the line
Me.Controls.Add(cb)
because that adds the checkbox to the tabpage, not the actual top tab bit. I'm still using the CheckBoxRenderer in the CheckedTabControl's OnDrawItem sub. Because this is just rendering the checkbox & not actually putting one there, I have to create a "hot-spot" on part of the tab to respond to. The code to change the state of the checkbox works fine - I can get it to respond to double clicks on an entire top tab bit, but not the specific part of a tab.
The code makes sense to me, and there are no runtime errors, but it just doesn't do anything!
 
Just in case anybody else was having similar problems,I've finally got the event thing to work. Here is my code in the CheckedTabControl Class:
VB.NET:
 Protected Overrides Sub OnMouseClick(ByVal e As System.Windows.Forms.MouseEventArgs)
        For i As Integer = 0 To Me.TabCount - 1

            Dim checkRect As New Rectangle(tabRect.Location, New Size(13, 13))
            Dim tabRect As Rectangle = Me.GetTabRect(i)
            If checkRect.Contains(e.Location) Then
                DirectCast(Me.TabPages(i), CheckedTabPage).ToggleCheck()
            End If
        Next i
    End Sub

The only issue left is the fact that I'm using OnDrawItem to render the checkbox, but this only gets triggered when the selected tab gets changed, not on mouse clicks (I only want the selected tab to change when clicking on the title of the tab rather than the checkbox element).
Is there a more elegant way of doing the re-rendering other than just triggering a redraw from any mouse click?
 
Ok, that was what you were doing, adding the checkbox to the tab flip (which is part of TabControl), I misunderstood and thought you meant the Tabpage..

About that last question, add Me.Refresh() after you toggle. Or Me.Invalidate(checkRect) since that is the only part you need to redraw. This can also be done from the IsChecked property Setter in tabpage class by accessing the tabpage Parent.

Avoiding tabpage change when only checkboxing one of the tabs can be done by detecting the checkrect hotspot on mouse down, if so cancel the tabpage change in Selecting event (e.cancel).

In a previous post you had this code: "If Not _TabPage.IsChecked Then ... CheckedNormal else UnCheckedNormal" - that is opposite of meaning.
 
Good spot on the "If Not..." bit! I only realized that last night too...
Also, thanks for the Me.Invalidate(checkRect) - not sure if its making any difference in terms of performance, but it has to be better than the Me.Refresh() that I was doing!

With the Selecting event, I know that e.Cancel will stop it, but how do I tie it in with the OnMouseClicked event? The Selecting event doesn't care about the mouse's location, and from what I understand, I can't parse additional data in (the region clicked). How would I do this short of having a global boolean variable in the class toggle allowed/disallowed switches?

But the biggest problem by far (and also the simplest) is the tab size. I ran into this problem at the very start, but I thought it'd be child's play to fix.
By rendering the Checkbox on the tab, I'm shifting the text to the right (off the end of the tab). I thought this would be easy to correct by changing the _TabPage.Width property and incrementing it by the Checkbox's width. However this doesn't seem to do anything! (I've placed the statement in the OnDrawItem sub).
I've also tried changing the width in Form1_Load with the same results.
The only way I can get it to work is if I align the text to the right of the tab, and and 4 extra spaces to the tab's name at creation (very very dodgy). Surely there's gotta be a better (/proper) way to do it? Is it because of the OwnerDrawFixed bit?
 
Invalidate(rectangle) is in some case very much better since with Refresh all control and all controls on it etc has to repaint, that may cause a lot of work for many.

Selecting: Click event is too late. As I said use the MouseDown event, it also got location. You have to use a Boolean between this and the Selecting to sync them.

OwnerDrawFixed doesn't support variable size tab flips.
 
Back
Top