The RadioButton Control
Master VB 2026's RadioButton — mutually exclusive selections, grouping with GroupBoxes and Panels, reading selected values, CheckedChanged events, iterating groups at runtime, combining RadioButtons with CheckBoxes, and Appearance = Button tab-strip patterns.
If / ElseIf tests on .Checked, or loop with Controls.OfType(Of RadioButton)() and find the one where .Checked = True. The CheckedChanged event fires twice per click — once for the button being unchecked (the old one) and once for the button being checked (the new one) — so always guard with If rb.Checked Then inside the handler.
22.1 Checked Property and CheckedChanged Event
RadioButtons in the same container are automatically mutually exclusive. You only need to check which one is Checked — VB deselects the others for you. In the CheckedChanged handler, always guard with If rb.Checked Then because the event fires once for the departing button (Checked becomes False) and once for the arriving button (Checked becomes True).
' --- Reading which button is selected (If/ElseIf chain) --- Private Sub btnConfirm_Click(...) Handles btnConfirm.Click Dim size As String If rbSmall.Checked Then size = "Small" ElseIf rbMedium.Checked Then size = "Medium" ElseIf rbLarge.Checked Then size = "Large" Else size = "(none selected)" End If lblOrder.Text = $"Size: {size}" End Sub ' --- Setting a default selection on Form_Load --- Private Sub Form1_Load(...) Handles MyBase.Load rbMedium.Checked = True ' sets Medium; Small and Large become False automatically End Sub ' --- CheckedChanged fires TWICE per click --- ' Once when the OLD button loses selection (Checked → False) ' Once when the NEW button gains selection (Checked → True) ' Guard with If rb.Checked Then to run code only once: Private Sub AnySize_CheckedChanged(...) Handles rbSmall.CheckedChanged, rbMedium.CheckedChanged, rbLarge.CheckedChanged Dim rb = DirectCast(sender, RadioButton) If Not rb.Checked Then Return ' ignore the "unchecked" fire Select Case rb.Name Case "rbSmall" : lblPrice.Text = "RM 8.00" Case "rbMedium" : lblPrice.Text = "RM 10.00" Case "rbLarge" : lblPrice.Text = "RM 12.50" End Select End Sub ' --- Using Tag to store numeric values instead of comparing strings --- ' In Designer, set rbSmall.Tag="8.00", rbMedium.Tag="10.00", rbLarge.Tag="12.50" Private Sub AnySize_CheckedChanged2(...) Handles rbSmall.CheckedChanged, rbMedium.CheckedChanged, rbLarge.CheckedChanged Dim rb = DirectCast(sender, RadioButton) If Not rb.Checked Then Return Dim price = CDec(rb.Tag) lblPrice.Text = price.ToString("C2") End Sub
When the user clicks a RadioButton, CheckedChanged fires twice: first for the previously-selected button (its Checked goes from True → False), then for the newly-selected button (False → True). If you forget the If Not rb.Checked Then Return guard, your handler runs twice on every click — often causing visible flicker or duplicate work. This is the single most common RadioButton mistake.
Select a pizza size. Only one can be active at a time. The price updates immediately via CheckedChanged, and the trace shows when the "double-fire" guard triggers.
22.2 Grouping with GroupBox and Panel
RadioButtons in the same direct parent container form one group. Place two GroupBoxes (or Panels) on the same form and they create two independent groups — VB treats them as separate radio families. This is essential for multi-question forms, order forms with multiple dimensions, and settings pages.
' Two independent groups — each GroupBox is its own selection scope. ' Selecting rbCheese in grpCrust has NO effect on grpSauce buttons. ' Reading both groups to build an order Private Sub btnOrder_Click(...) Handles btnOrder.Click Dim crust = GetSelected(grpCrust) ' returns Text of checked button Dim sauce = GetSelected(grpSauce) Dim size = GetSelected(grpSize) lblSummary.Text = $"{size} pizza, {crust} crust, {sauce} sauce" End Sub ' Generic helper: find checked button in any container Private Function GetSelected(container As Control) As String Dim rb = container.Controls.OfType(Of RadioButton)() _ .FirstOrDefault(Function(r) r.Checked) Return If(rb IsNot Nothing, rb.Text, "(none)") End Function ' Generic helper using Tag value instead of Text Private Function GetSelectedTag(Of T)(container As Control) As T Dim rb = container.Controls.OfType(Of RadioButton)() _ .FirstOrDefault(Function(r) r.Checked) If rb IsNot Nothing AndAlso rb.Tag IsNot Nothing Then Return CType(rb.Tag, T) End If Return Nothing End Function ' Programmatically select a button by value Private Sub SelectByTag(container As Control, tagValue As Object) For Each rb In container.Controls.OfType(Of RadioButton)() If rb.Tag?.ToString() = tagValue.ToString() Then rb.Checked = True Return End If Next End Sub
Three independent GroupBoxes on one form. Selecting within one group never affects the others. The order summary reads all three groups using the GetSelected helper.
22.3 Iterating RadioButton Groups at Runtime
When groups are built dynamically (loaded from a database, config file, or survey engine), you cannot hard-code control names. Use Controls.OfType(Of RadioButton)() to scan a container at runtime to find what is selected or to build the group programmatically.
' Build a group of RadioButtons at runtime from a list of options Private Sub BuildRadioGroup(panel As Panel, options() As String, defaultIndex As Integer = 0) panel.Controls.Clear() Dim y = 8 For i = 0 To options.Length - 1 Dim rb As New RadioButton With { .Text = options(i), .Tag = i, .Checked = (i = defaultIndex), .Location = New Point(8, y), .AutoSize = True } AddHandler rb.CheckedChanged, AddressOf DynamicRb_CheckedChanged panel.Controls.Add(rb) y += 26 Next End Sub Private Sub DynamicRb_CheckedChanged(sender As Object, e As EventArgs) Dim rb = DirectCast(sender, RadioButton) If Not rb.Checked Then Return ' double-fire guard lblResult.Text = $"Selected: {rb.Text} (index {rb.Tag})" End Sub ' Get selected index from any RadioButton group panel Private Function GetSelectedIndex(panel As Panel) As Integer Dim rb = panel.Controls.OfType(Of RadioButton)() _ .FirstOrDefault(Function(r) r.Checked) Return If(rb IsNot Nothing, CInt(rb.Tag), -1) End Function ' Load survey questions from a list Dim satisfactionOptions = {"Very Satisfied", "Satisfied", "Neutral", "Dissatisfied"} BuildRadioGroup(pnlSatisfaction, satisfactionOptions, defaultIndex:=1) ' Save a survey response Private Sub btnSubmit_Click(...) Handles btnSubmit.Click Dim q1 = GetSelectedIndex(pnlQ1) Dim q2 = GetSelectedIndex(pnlQ2) Dim q3 = GetSelectedIndex(pnlQ3) If q1 = -1 OrElse q2 = -1 OrElse q3 = -1 Then MessageBox.Show("Please answer all questions.") Return End If SaveResponse(q1, q2, q3) End Sub
A 3-question survey with RadioButton groups built from data arrays — just like BuildRadioGroup above. Submit validates all three are answered and shows the VB handler trace.
22.4 Appearance = Button — Tab-Strip Pattern
Setting Appearance = Appearance.Button on RadioButtons inside a Panel creates a professional tab-strip or segmented control. The selected button stays "pressed" while others appear raised. This pattern is common in settings pages, filter bars, and view-switchers.
' In the Designer: ' 1. Add a Panel (pnlTabs) with FlowLayout, horizontal ' 2. Add RadioButtons: rbDay, rbWeek, rbMonth, rbYear ' 3. Set ALL to Appearance = Appearance.Button, FlatStyle = Flat ' 4. Style with BackColor, ForeColor as needed ' 5. They share pnlTabs as container → mutually exclusive automatically Private Sub Form1_Load(...) Handles MyBase.Load rbDay.Checked = True ' default tab End Sub Private Sub AnyTab_CheckedChanged(...) Handles rbDay.CheckedChanged, rbWeek.CheckedChanged, rbMonth.CheckedChanged, rbYear.CheckedChanged Dim rb = DirectCast(sender, RadioButton) If Not rb.Checked Then Return ' Hide all content panels, show the relevant one pnlDay.Visible = rbDay.Checked pnlWeek.Visible = rbWeek.Checked pnlMonth.Visible = rbMonth.Checked pnlYear.Visible = rbYear.Checked ' Or use Select Case on rb.Name: Select Case rb.Name Case "rbDay" : LoadDayData() Case "rbWeek" : LoadWeekData() Case "rbMonth" : LoadMonthData() Case "rbYear" : LoadYearData() End Select End Sub
Four RadioButtons with Appearance = Button style act as tabs. Selecting a tab switches the visible content panel and loads data — just as AnyTab_CheckedChanged would work in a real WinForms app.
22.5 Combining RadioButtons and CheckBoxes
Real forms mix both controls. RadioButtons choose one mandatory option (size, plan tier, payment method); CheckBoxes add optional extras. A common pattern is using a RadioButton to enable a whole section, which in turn enables or disables a group of CheckBoxes.
' Subscription plan form: ' RadioButtons choose the plan tier (Basic / Pro / Enterprise) ' CheckBoxes choose optional add-ons (some disabled for Basic) Private ReadOnly _plans As New Dictionary(Of String, Decimal) From { {"Basic", 29.0}, {"Pro", 79.0}, {"Enterprise", 199.0} } Private ReadOnly _addons As New Dictionary(Of String, Decimal) From { {"Extra Storage", 10.0}, {"Priority Support", 20.0}, {"Analytics", 15.0} } Private Sub AnyPlan_CheckedChanged(...) Handles rbBasic.CheckedChanged, rbPro.CheckedChanged, rbEnterprise.CheckedChanged Dim rb = DirectCast(sender, RadioButton) If Not rb.Checked Then Return ' Basic plan cannot have add-ons Dim isBasic = rbBasic.Checked chkStorage.Enabled = Not isBasic chkSupport.Enabled = Not isBasic chkAnalytics.Enabled = Not isBasic If isBasic Then chkStorage.Checked = False chkSupport.Checked = False chkAnalytics.Checked = False End If UpdateTotal() End Sub Private Sub UpdateTotal() Dim planName = GetSelected(grpPlan) Dim total = _plans(planName) If chkStorage.Checked Then total += _addons("Extra Storage") If chkSupport.Checked Then total += _addons("Priority Support") If chkAnalytics.Checked Then total += _addons("Analytics") lblTotal.Text = $"Monthly total: {total:C2}" End Sub
RadioButtons choose the plan tier; CheckBoxes add optional extras. The Basic plan disables all add-ons. Price updates instantly on every change. The VB code trace shows which .Enabled assignments fire on each plan switch.
22.6 GitHub Copilot — RadioButton Patterns
' Build a multiple-choice quiz engine. Load questions from a List(Of QuizQuestion). Each question has Text and an array of Answers. Build RadioButtons at runtime in a Panel, handle CheckedChanged to track selections, score on Submit.'' Multiple-choice quiz engine with runtime RadioButton generation. Structure QuizQuestion Dim Text As String Dim Answers() As String Dim CorrectIndex As Integer End Structure Private _questions As List(Of QuizQuestion) Private _panels As New List(Of Panel) Private _current As Integer = 0 Private Sub LoadQuestion(index As Integer) pnlQuestion.Controls.Clear() Dim q = _questions(index) Dim y = 0 pnlQuestion.Controls.Add(New Label With { .Text = $"Q{index+1}: {q.Text}", .AutoSize = True, .Font = New Font("Segoe UI", 10, FontStyle.Bold), .Location = New Point(0, y)}) y += 30 For i = 0 To q.Answers.Length - 1 Dim rb As New RadioButton With { .Text = q.Answers(i), .Tag = i, .Location = New Point(0, y), .AutoSize = True } AddHandler rb.CheckedChanged, AddressOf Answer_CheckedChanged pnlQuestion.Controls.Add(rb) y += 26 Next lblProgress.Text = $"Question {index+1} of {_questions.Count}" End Sub Private Sub Answer_CheckedChanged(sender As Object, e As EventArgs) Dim rb = DirectCast(sender, RadioButton) If Not rb.Checked Then Return btnNext.Enabled = True ' enable Next only once answered End Sub Private Sub btnNext_Click(...) Handles btnNext.Click Dim selected = pnlQuestion.Controls.OfType(Of RadioButton)() _ .FirstOrDefault(Function(r) r.Checked) If selected IsNot Nothing AndAlso CInt(selected.Tag) = _questions(_current).CorrectIndex Then _score += 1 End If _current += 1 If _current < _questions.Count Then LoadQuestion(_current) Else MessageBox.Show($"Score: {_score}/{_questions.Count}") End If End Sub
Try these in the Copilot Chat panel while working with RadioButtons:
- "Write a generic Function GetCheckedRadio(container As Control) As RadioButton that returns the checked RadioButton or Nothing if none is selected, using LINQ OfType and FirstOrDefault"
- "Build a shipping calculator: RadioButtons for Standard/Express/Overnight rates, CheckBoxes for insurance and signature-required add-ons, Label showing total formatted C2 updating on every CheckedChanged"
- "Create a settings form with four tab-strip RadioButtons (Appearance=Button) that each show/hide a different Panel using a shared CheckedChanged handler and Select Case rb.Name"
- "Implement a payment method selector: Radio buttons for Credit Card / Bank Transfer / eWallet, each unhiding a different sub-panel with the relevant input fields"
Lesson Summary
- RadioButtons in the same container are automatically mutually exclusive — selecting one deselects all others in that container. No code is needed to implement this behaviour.
- Use separate GroupBox or Panel containers to create multiple independent groups on one form. Each container is its own selection scope.
CheckedChangedfires twice per click — once for the deselected button (Checked → False) and once for the newly selected button (Checked → True). Always guard withIf Not rb.Checked Then Returnat the top of the handler.- Use a shared handler with
DirectCast(sender, RadioButton)to handle all buttons in a group with one Sub. List them all afterHandles, separated by commas. - Store values in the
Tagproperty (price, enum index, ID) instead of comparing.Textstrings — this makes the code locale-independent and easier to maintain. - Loop with
container.Controls.OfType(Of RadioButton)().FirstOrDefault(Function(r) r.Checked)to find the selected button without hard-coding names — essential for dynamically built groups. Appearance = Appearance.Buttoncreates visually pressed toggle buttons — use them inside a Panel for a professional tab-strip or segmented control. Mutual exclusivity still applies automatically.
Exercises
Exercise 22.1 — Hotel Room Booking
- Create three GroupBoxes: Room Type (Standard/Deluxe/Suite), Duration (1 night/Weekend/Week), and Meal Plan (None/Breakfast/Half-Board/Full-Board)
- Use a shared
CheckedChangedhandler withIf Not rb.Checked Then Returnguard - Calculate total cost using a Dictionary keyed by Tag values
- Show a formatted booking summary with
String.Formatalignment (Lesson 20) - Copilot challenge: Ask Copilot to "add early-bird discount: if Duration is Weekend or Week, apply 10% discount and show saving formatted C2"
Exercise 22.2 — Font Selector
- Build a real-time font preview: RadioButtons for font family, RadioButtons for size, CheckBoxes for Bold/Italic/Underline
- Apply all selections to a sample TextBox Label using
New Font(...) - Use
Appearance = Buttonfor the size options as a tab-strip (8pt / 10pt / 12pt / 16pt / 24pt) - Disable Italic for monospace fonts (Courier New, Consolas) using the CheckedChanged handler
- Copilot challenge: Ask Copilot to "serialize the selected font settings to JSON using System.Text.Json and restore them on Form_Load"
Exercise 22.3 — Survey Engine
- Store 5 questions in a
List(Of QuizQuestion)Structure, each with 4 answer options and a correct index - Build RadioButtons at runtime using
AddHandlerand a dynamic Panel - Navigate Next/Previous, tracking answers in an Integer array
- Show results page: score, percentage (
P0), and per-question correct/wrong indicator - Copilot challenge: Ask Copilot to "add a progress bar that fills as questions are answered, and colour answered questions green in a sidebar list"
Related Resources
← Lesson 21
CheckBox — independent multi-select options.
Lesson 23 →
Web Browser — embed WebView2 in WinForms.
MS Docs — RadioButton
Complete API reference for the RadioButton control in .NET 10.
MS Docs — GroupBox
Grouping controls for mutual exclusivity and visual organisation.
Featured Books
Visual Basic 2022 Made Easy
RadioButton, CheckBox, and all WinForms controls covered with complete working programs.
View on Amazon →
VB Programming With Code Examples
Order forms, settings dialogs, and survey engines using RadioButtons throughout.
View on Amazon →