Forms in VB.Net are used as containers for Controls. Both Forms + Controls have Properties, Events, + Methods.
In this chapter I'll show you how to use simple Controls to accept + validate user input + display the results of calculations in smart, eye catching + effective user interfaces, + how to create functional user customizable layouts for your forms.
Later in this chapter we'll look at extending Controls through Inheritance, + creating custom Usercontrols to use in your applications.
I've changed the Name Property for the Form, 3 Textboxes, Checkbox, Listview, + Button.
I've also changed these additional Properties for the Form:
And this Property for the Listview:
Then added 4 columns to the Listview + changed their ColumnHeader text.
Here's the code for the example Project:
Public Class frmFastFood Private Sub frmFastFood_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load 'prompt user to enter some numbers askForInput() End Sub Private Sub askForInput() MessageBox.Show("Enter numbers in textboxes", Me.Text, MessageBoxButtons.OK, MessageBoxIcon.Information) End Sub Private Sub btnCompute_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnCompute.Click Dim pizza As Integer = 0 Dim fries As Integer = 0 Dim drinks As Integer = 0 Dim delivery As Decimal = If(chkDelivery.Checked, 2.5, 0) 'if all 3 textboxes contain whole numbers... 'display itemized bill in the listview If Integer.TryParse(txtPizzaSlices.Text, pizza) AndAlso Integer.TryParse(txtFrenchFries.Text, fries) AndAlso Integer.TryParse(txtDrinks.Text, drinks) Then 'this calls the itemize procedure + also calls the computeTotal function as 1 of the parameters itemize(pizza, fries, drinks, delivery, computeTotal(pizza, fries, drinks, delivery).ToString("c2")) End If End Sub Private Function computeTotal(ByVal pizzaSlices As Integer, ByVal frenchFries As Integer, ByVal softDrinks As Integer, ByVal delivery As Decimal) As Decimal Return (pizzaSlices * 1.75) + (frenchFries * 2) + (softDrinks * 1.25) + delivery End Function Private Sub itemize(ByVal pizzaSlices As Integer, ByVal frenchFries As Integer, ByVal softDrinks As Integer, ByVal delivery As Decimal, ByVal total As String) 'this clears the listview, creates a List(Of ListViewItem) + repopulates the listview lvBill.Items.Clear() Dim lvi As New List(Of ListViewItem) If pizzaSlices > 0 Then lvi.Add(New ListViewItem(New String() {"Pizza slices", pizzaSlices, CDec(1.75).ToString("c2"), CDec(1.75 * pizzaSlices).ToString("c2")})) If frenchFries > 0 Then lvi.Add(New ListViewItem(New String() {"Fries", frenchFries, CDec(2).ToString("c2"), CDec(2 * frenchFries).ToString("c2")})) If softDrinks > 0 Then lvi.Add(New ListViewItem(New String() {"Soft drinks", softDrinks, CDec(1.25).ToString("c2"), CDec(1.25 * softDrinks).ToString("c2")})) lvi.Add(New ListViewItem("")) If delivery > 0 Then lvi.Add(New ListViewItem(New String() {"", "", "Delivery:", delivery.ToString("c2")})) lvi.Add(New ListViewItem(New String() {"", "", "Total:", total})) lvBill.Items.AddRange(lvi.ToArray) End Sub End Class
You can download the first example Project here: pizzas.zip
This project demonstrates some of the similarities + differences between the CheckedListbox + Listbox controls, + also shows the effects of docking Controls. Run the example project + maximize the Form to see how Docking works. This project also uses SplitContainers which make the Form layout customizable by the user.
I've changed the Form, CheckedListbox, Listbox, 2 Checkboxes, + 2 Textboxes Name Property.
The Form has these additional properties:
In addition to the obviously visible Controls, there are 3 SplitContainers + a TableLayoutPanel.
The first SplitContainer is Docked to the top of the Form, with each Panel containing another SplitContainer, whose Panels contain a CheckedListbox + a Textbox, + a Listbox + a Textbox.
These are the relevant Properties from the first SplitContainer:
These are the relevant Properties which both child SplitContainers share:
The TableLayoutPanel has 1 row + 4 columns. The columns are 2 fixed 12 pixel wide columns, + 2 of 50% of the remaining width.
These are the relevant Properties from the TableLayoutPanel:
To set these Properties you'd right click the TableLayoutPanel, + choose Edit Rows + Columns... which will open this Window:
The CheckedListbox, Listbox, + 2 Textboxes all have their Dock Property set to DockStyle.Fill, which means they fill their parent container.
Here's the code for the example Project:
Public Class frmControls Private Sub frmControls_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load 'add some items to the checkedListbox + the listbox clbItems.Items.AddRange(New String() {"One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten"}) lbItems.Items.AddRange(New String() {"One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten"}) End Sub Private Sub clbItems_MouseUp(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles clbItems.MouseUp 'depending on the value of chkMultiCheckedItems.Checked If chkMultiCheckedItems.Checked Then 'display all of the CheckedItems' text in the textbox txtOutput.Lines = clbItems.CheckedItems.Cast(Of String).ToArray Else 'display the selectedindex + selecteditem in the textbox txtOutput.Lines = New String() {String.Format("SelectedIndex: {0}", clbItems.SelectedIndex), _ String.Format("SelectedItem: {0}", If(clbItems.SelectedIndex > -1, clbItems.SelectedItem.ToString, ""))} End If End Sub Private Sub lbItems_SelectedIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lbItems.SelectedIndexChanged 'depending on the value of chkMultiSelect.Checked If chkMultiSelect.Checked Then 'display all of the SelectedItems' text in the textbox txtOutput2.Lines = lbItems.SelectedItems.Cast(Of String).ToArray Else 'display the selectedindex + selecteditem in the textbox txtOutput2.Lines = New String() {String.Format("SelectedIndex: {0}", lbItems.SelectedIndex), _ String.Format("SelectedItem: {0}", If(lbItems.SelectedIndex > -1, lbItems.SelectedItem.ToString, ""))} End If End Sub Private Sub chkMultiSelect_CheckedChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles chkMultiSelect.CheckedChanged If chkMultiSelect.Checked Then 'change the listbox SelectionMode + update the textbox lbItems.SelectionMode = SelectionMode.MultiSimple txtOutput2.Lines = lbItems.SelectedItems.Cast(Of String).ToArray Else 'deselect all SelectedItems + change the listbox SelectionMode lbItems.SelectedItems.Clear() lbItems.SelectionMode = SelectionMode.One End If End Sub Private Sub clbItems_SelectedIndexChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles clbItems.SelectedIndexChanged If Not chkMultiCheckedItems.Checked Then 'unCheck all checkedItems except the current SelectedIndex For x As Integer = 0 To clbItems.Items.Count - 1 If x <> clbItems.SelectedIndex Then clbItems.SetItemChecked(x, False) End If Next End If End Sub Private Sub chkMultiCheckedItems_CheckedChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles chkMultiCheckedItems.CheckedChanged If chkMultiCheckedItems.Checked Then 'display all of the CheckedItems' text in the textbox txtOutput.Lines = clbItems.CheckedItems.Cast(Of String).ToArray Else 'unCheck all checkedItems For x As Integer = 0 To clbItems.Items.Count - 1 clbItems.SetItemChecked(x, False) Next 'display the selectedindex + selecteditem in the textbox txtOutput.Lines = New String() {String.Format("SelectedIndex: {0}", clbItems.SelectedIndex), _ String.Format("SelectedItem: {0}", If(clbItems.SelectedIndex > -1, clbItems.SelectedItem.ToString, ""))} End If End Sub End Class
You can download the second example Project here: Simple Controls.zip
I've changed the Form's + all of the Control's Name from the default names.
I've also changed these additional Properties for the Form:
And this Property for the Listview:
Then added a column to the Listview.
The Form also has 2 ImageList Components + the Project has 2 images in My.Resources (Project-->Properties-->Resources), that are used dynamically in the code.
Here's the code for the example Project:
Public Class frmReminder 'reminders index variable Dim reminders As Integer = 0 <System.Serializable()> _ Public Structure item Public text As String Public tag As Object End Structure Private Sub frmReminder_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing ' 'this saves your reminders to a binary file 'using binary serialization ' Dim items As List(Of item) = (From i As ListViewItem In lvList.Items.Cast(Of ListViewItem)() Select New item With {.text = i.Text, .tag = i.Tag}).ToList Dim formatter As New Runtime.Serialization.Formatters.Binary.BinaryFormatter Dim fs As New IO.FileStream("notes.bin", IO.FileMode.Create) formatter.Serialize(fs, items) fs.Close() End Sub Private Sub frmReminder_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load 'set some more listview properties lvList.HeaderStyle = ColumnHeaderStyle.None lvList.HideSelection = False ' 'this reads + loads any saved reminders ' Dim formatter As New Runtime.Serialization.Formatters.Binary.BinaryFormatter Dim fs As New IO.FileStream("notes.bin", IO.FileMode.Open) Dim items As List(Of item) = DirectCast(formatter.Deserialize(fs), Global.System.Collections.Generic.List(Of item)) fs.Close() For Each i In items btnAdd.PerformClick() lvList.Items(lvList.Items.Count - 1).Text = i.text lvList.Items(lvList.Items.Count - 1).Tag = i.tag Next lvList.SelectedIndices.Clear() End Sub Private Sub btnAdd_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnAdd.Click 'add item 'create the listview index images. standard + wide sizes Dim img As New Bitmap(My.Resources.blank) Dim imgWide As New Bitmap(My.Resources.blankWide) Dim gr() As Graphics = {Graphics.FromImage(img), Graphics.FromImage(imgWide)} Dim textSize As SizeF = gr(0).MeasureString(reminders.ToString, Me.Font) gr(0).DrawString(reminders.ToString, Me.Font, Brushes.Black, (img.Width - textSize.Width) / 2, (img.Height - textSize.Height) / 2) gr(1).DrawString(reminders.ToString, Me.Font, Brushes.Black, (imgWide.Width - textSize.Width) / 2, (imgWide.Height - textSize.Height) / 2) 'add the standard image to imageList1 ImageList1.Images.Add(img) 'add the wide image to imageList2 ImageList2.Images.Add(imgWide) 'set the listview SmallImageList property If reminders >= 10 Then lvList.SmallImageList = ImageList2 Else lvList.SmallImageList = ImageList1 End If 'increment the reminders variable reminders += 1 'add the reminder to the listview lvList.Invalidate() lvList.Items.Add("{New Reminder" & reminders.ToString & "}", ImageList2.Images.Count - 1) 'select the new reminder lvList.Items(lvList.Items.Count - 1).Selected = True End Sub Private Sub btnRemove_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnRemove.Click 'remove item If lvList.SelectedItems.Count = 1 Then 'get the index of the SelectedItem Dim index As Integer = lvList.SelectedIndices(0) 'remove the SelectedItem lvList.Items.RemoveAt(index) 'rearrange the listview images For x As Integer = 0 To lvList.Items.Count - 1 lvList.Items(x).ImageIndex = x Next 'remove the last image from both imageLists ImageList2.Images.RemoveAt(ImageList2.Images.Count - 1) ImageList1.Images.RemoveAt(ImageList1.Images.Count - 1) 'decrement the reminders variable reminders -= 1 'set the listview SmallImageList property If reminders > 10 Then lvList.SmallImageList = ImageList2 Else lvList.SmallImageList = ImageList1 End If 'depending on the index of the removed item 'select either the preceding item or the next item lvList.Invalidate() If lvList.Items.Count > 0 Then If index < lvList.Items.Count Then lvList.Items(index).Selected = True Else lvList.Items(index - 1).Selected = True End If End If End If End Sub Private Sub btnMoveUp_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnMoveUp.Click 'move up 'remove the selectedItem from the list + reinsert 1 place higher Dim index As Integer = lvList.SelectedIndices(0) Dim text As String = lvList.Items(index).Text Dim tag As Object = lvList.Items(index).Tag lvList.Items.RemoveAt(index) lvList.Items.Insert(index - 1, text) lvList.Items(index - 1).Tag = tag 'rearrange the listview images For x As Integer = 0 To lvList.Items.Count - 1 lvList.Items(x).ImageIndex = x Next 'select the newly inserted reminder lvList.Items(index - 1).Selected = True End Sub Private Sub btnMoveDown_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnMoveDown.Click 'move down 'remove the selectedItem from the list + reinsert 1 place lower Dim index As Integer = lvList.SelectedIndices(0) Dim text As String = lvList.Items(index).Text Dim tag As Object = lvList.Items(index).Tag lvList.Items.RemoveAt(index) lvList.Items.Insert(index + 1, text) lvList.Items(index + 1).Tag = tag 'rearrange the listview images For x As Integer = 0 To lvList.Items.Count - 1 lvList.Items(x).ImageIndex = x Next 'select the newly inserted reminder lvList.Items(index + 1).Selected = True End Sub Private Sub lvList_SelectedIndexChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles lvList.SelectedIndexChanged If lvList.SelectedItems.Count = 1 Then 'enable/disable the buttons depending on the SelectedItems.Count '+ the position of the SelectedItem in the list btnMoveUp.Enabled = lvList.SelectedIndices(0) > 0 btnMoveDown.Enabled = lvList.SelectedIndices(0) < lvList.Items.Count - 1 btnRemove.Enabled = True txtNote.Enabled = True 'get the tag property from the SelectedItem + display in the textbox Dim txtObject As Object = lvList.Items(lvList.SelectedIndices(0)).Tag txtNote.Text = If(txtObject IsNot Nothing, txtObject.ToString, "") Else 'disable all of the buttons (except btnAdd) + clear the textbox + disable it btnMoveUp.Enabled = False btnMoveDown.Enabled = False btnRemove.Enabled = False txtNote.Enabled = False txtNote.Text = "" End If End Sub Private Sub txtNote_TextChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles txtNote.TextChanged If lvList.SelectedItems.Count = 1 Then 'save txtNote.Text to the lvList.SelectedItem's tag property Dim index As Integer = lvList.SelectedIndices(0) lvList.Items(index).Tag = txtNote.Text End If End Sub End Class
You can download the third example Project here: Reminders.zip
I've changed the Form's + all of the Control's Name from the default names.
I've also changed these additional Properties for the Form:
Also, I added 4 columns (a TextboxColumn, a ComboboxColumn, a CheckboxColumn, + another TextboxColumn) to the Datagridview. The last (TextboxColumn) is Readonly.
This is the code for the main Form:
Public Class frmInput 'this boolean flag variable is used to avoid calculating the total before the form is loaded Dim loading As Boolean = True 'this is a form level array, so you only declare + load it once 'arrays declared locally within a procedure are redeclared every time the procedure runs Dim values() As Integer = {0, 695, 545, 545, 545, 480, 480, 480, 480, 395, 395, 395, 395, 395, 395, 395, 395} Private Sub frmInput_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing 'save datagridview contents dgvInput.saveToXML("data.xml") 'user defined extension method End Sub Private Sub frmInput_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load 'bind the datagridviewcombobox column Dim dgvComboboxes As DataGridViewComboBoxColumn = DirectCast(dgvInput.Columns(1), DataGridViewComboBoxColumn) dgvComboboxes.DataSource = New String() {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16"} 'load any saved data dgvInput.loadFromXML("data.xml") 'user defined extension method loading = False End Sub Private Sub DGVComboIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) 'this handles the datagridviewcombobox cell selectedindexchanged event Dim cb As ComboBox = DirectCast(sender, ComboBox) 'calculate the cost of 1 company sending x amount of employees to conference calculateCost(dgvInput.CurrentCell.RowIndex, cb) 'calculate the total for all rows in the datagridview calculateTotal() End Sub Private Sub dgvInput_CellValueChanged(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewCellEventArgs) Handles dgvInput.CellValueChanged If Not loading Then 'if sender is the checkbox column If e.ColumnIndex = 2 Then 'calculate the cost of 1 company sending x amount of employees to conference calculateCost(e.RowIndex) 'calculate the total for all rows in the datagridview calculateTotal() End If End If End Sub Private Sub dgvInput_CurrentCellDirtyStateChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles dgvInput.CurrentCellDirtyStateChanged 'if CurrentCell is the checkbox column 'this is necessary to trigger the dgvInput_CellValueChanged event If dgvInput.CurrentCell.ColumnIndex = 2 Then dgvInput.CommitEdit(DataGridViewDataErrorContexts.Commit) End If End Sub Private Sub dgvInput_EditingControlShowing(ByVal sender As Object, _ ByVal e As System.Windows.Forms.DataGridViewEditingControlShowingEventArgs) Handles dgvInput.EditingControlShowing ' Attempt to cast the EditingControl to a ComboBox. 'this will only work if CurrentCell is the combobox column Dim cb As ComboBox = TryCast(e.Control, ComboBox) 'if it is the combobox column... If cb IsNot Nothing Then RemoveHandler cb.SelectedIndexChanged, AddressOf DGVComboIndexChanged AddHandler cb.SelectedIndexChanged, AddressOf DGVComboIndexChanged End If End Sub Private Sub dgvInput_RowsAdded(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewRowsAddedEventArgs) Handles dgvInput.RowsAdded 'calculate the total for all rows in the datagridview calculateTotal() End Sub Private Sub dgvInput_RowsRemoved(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewRowsRemovedEventArgs) Handles dgvInput.RowsRemoved 'calculate the total for all rows in the datagridview calculateTotal() End Sub Private Sub calculateCost(ByVal rowIndex As Integer, Optional ByVal cb As ComboBox = Nothing) If cb Is Nothing Then 'if the optional cb parameter is omitted... 'the formula is number of attendees * cost for that many attendees, 'which is stored in the form level values() array dgvInput(3, rowIndex).Value = If(dgvInput(2, rowIndex).Value IsNot Nothing AndAlso CBool(dgvInput(2, rowIndex).Value) = True, _ ((CInt(dgvInput(1, rowIndex).Value) * values(CInt(dgvInput(1, rowIndex).Value))) * 0.85).ToString("c2"), _ (CInt(dgvInput(1, rowIndex).Value) * values(CInt(dgvInput(1, rowIndex).Value))).ToString("c2")) Else dgvInput(3, rowIndex).Value = If(dgvInput(2, rowIndex).Value IsNot Nothing AndAlso CBool(dgvInput(2, rowIndex).Value) = True, _ ((CInt(cb.SelectedItem) * values(CInt(cb.SelectedItem))) * 0.85).ToString("c2"), _ (CInt(cb.SelectedItem) * values(CInt(cb.SelectedItem))).ToString("c2")) End If End Sub Private Sub calculateTotal() 'this totals the datagridview rows (Total Fee column) Dim total = (From row As DataGridViewRow In dgvInput.Rows.Cast(Of DataGridViewRow)() _ Select If(row.IsNewRow OrElse row.Cells(3).Value Is Nothing, 0, CDec(row.Cells(3).Value.ToString))).Sum '+ displays the total in lblTotal formatted as a decimal 'in your local currency with 2 decimal places lblTotal.Text = String.Format("Total: {0:c2}", total) End Sub End Class
This example Project's startup Form is a Splashscreen. It loads, remains on screen for 3 seconds, then gradually fades away, before loading the main Form.
The timing for the Splashscreen duration + fading is controlled by 2 Timer Components.
To create a Splashscreen from a standard Form, there are Properties you'll need to set:
But if you use a template, most of the Properties are setup already.
Here's the code for the Splashscreen:
Public NotInheritable Class frmSplashScreen Private Sub frmSplashScreen_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load 'this makes all parts of the form that are red, transparent 'but it's still got red parts as VB doesn't display colors perfectly some times - 'as there are 256^3 colors in vb and the color has to be exactly the same as the TransparencyKey. 'you can use any form as a splashscreen, either the standard splashscreen '(from the Add New Item form) or your own custom form. Me.TransparencyKey = Color.Red End Sub Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Tick 'after 3 seconds, timer1 ticks, then disables itself + starts timer2 Timer1.Enabled = False Timer2.Enabled = True End Sub Private Sub Timer2_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer2.Tick 'this fades out the splashscreen Me.Opacity -= 0.1 If Me.Opacity = 0 Then frmInput.Show() Me.Close() End If End Sub End Class
This Splashscreen is partially transparent at runtime.
This Project uses Extension methods. 2 methods are added to the Datagridview control. These apply to any instance of a Datagridview in the Project.
Here's the code for the Extension methods:
''' <summary> ''' extensions module ''' </summary> ''' <remarks>these are extensions for the datagridview. ''' ideally you should only create reusable extension methods ''' whereas these are specialized, but they demonstrate the technique</remarks> Module extensions <System.Runtime.CompilerServices.Extension()> _ Public Sub saveToXML(ByVal instance As DataGridView, ByVal xmlFilename As String) 'in an extension method the first parameter refers to the class being extended. 'this creates + saves an xml file using the datagridview rows + the xmlFilename specified Dim xml = _ <?xml version="1.0" standalone="no"?> <DGV> </DGV> For x As Integer = 0 To instance.Rows.Count - 1 If Not instance.Rows(x).IsNewRow Then If instance.Rows(x).Cells(0).Value Is Nothing Then Continue For xml...<DGV>(0).Add( _ <DataGridViewRow> <Value1><%= instance.Rows(x).Cells(0).Value.ToString %></Value1> <Value2><%= If(instance.Rows(x).Cells(1).Value Is Nothing, "", instance.Rows(x).Cells(1).Value.ToString) %></Value2> <Value3><%= If(instance.Rows(x).Cells(2).Value Is Nothing, CStr(False), instance.Rows(x).Cells(2).Value.ToString) %></Value3> <Value4><%= If(instance.Rows(x).Cells(3).Value Is Nothing, "", instance.Rows(x).Cells(3).Value.ToString) %></Value4> </DataGridViewRow>) End If Next xml.Save(xmlFilename) End Sub <System.Runtime.CompilerServices.Extension()> _ Public Sub loadFromXML(ByVal instance As DataGridView, ByVal xmlFilename As String) If Not IO.File.Exists(xmlFilename) Then Return 'this extension method reads the specified xml file + loads the values into the DataGridView Dim xml As XDocument = XDocument.Load(xmlFilename) Dim rows = (From node In xml...<DataGridViewRow> _ Select New Object() {node...<Value1>.Value, _ node...<Value2>.Value, _ node...<Value3>.Value, _ node...<Value4>.Value}).ToArray For Each r In rows instance.Rows.Add(r) Next End Sub End Module
You can download the example Project here: Conference Bookings.zip
I've changed the Form's + all of the Control's Name from the default names.
I've also changed these additional Properties for the Form:
And this Property for the Listview:
Then added 3 columns to the Listview + changed their ColumnHeader Text. The promptTextboxes have a prompt Property + an isInteger Property that you need to set too.
Here's the code for the example Project:
Public Class frmGrades 'form level array 'only needs to be declared + loaded once in lifetime of form Dim grades(,) As Object Private Sub frmGrades_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load 'initialize the promptTextboxes ptbName.init() ptbExamScore1.init() ptbExamScore2.init() ptbCourseWorkScore.init() 'load the grades array grades = New Object(,) {{100, "A+"}, {95, "A"}, {85, "B"}, {75, "C"}, {65, "D"}, {55, "E"}, {50, "U"}} End Sub Private Sub btnCalculateGrade_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnCalculateGrade.Click 'declare + load new array of promptTextboxes Dim ptbs() As promptTextbox = {ptbExamScore1, ptbExamScore2, ptbCourseWorkScore} 'calculate average score from promptTextbox values Dim average As Double = ptbs.Average(Function(p As promptTextbox) p.score) 'calculate grade from average score Dim firstList As List(Of String) = Enumerable.Range(0, grades.GetLength(0)).Select(Function(i) If(CDbl(grades(i, 0)) > average, CStr(grades(i, 1)), "")).ToList firstList.RemoveAll(Function(s) s = "") Dim grade As String = firstList.LastOrDefault 'add name, average score, + grade to listview lvList.Items.Add(New ListViewItem(New String() {ptbName.Text, average.ToString("f1"), grade})) End Sub End Class
This is the code for the Extended Textbox Control:
''' <summary> ''' extended textbox control ''' </summary> ''' <remarks></remarks> Public Class promptTextbox Inherits TextBox 'class level variable - available throughout the class Dim oldText As String 'class level constant Const WM_PASTE As Integer = 770 Protected Overrides Sub OnKeyDown(ByVal e As System.Windows.Forms.KeyEventArgs) 'don't allow arrow keys If e.KeyCode = Keys.Left OrElse e.KeyCode = Keys.Up OrElse e.KeyCode = Keys.Right OrElse e.KeyCode = Keys.Down Then e.SuppressKeyPress = True End If MyBase.OnKeyDown(e) 'set variable used in OnKeyUp procedure oldText = MyBase.Text End Sub Protected Overrides Sub OnKeyUp(ByVal e As System.Windows.Forms.KeyEventArgs) 'handles Delete + Back Keys 'to determine whether to display prompt or not If e.KeyCode = Keys.Delete Then showHidePrompt(oldText, MyBase.Text.Remove(MyBase.SelectionStart, MyBase.SelectionLength)) ElseIf e.KeyCode = Keys.Back Then Dim newText As String = "" If MyBase.SelectionLength > 0 Then newText = MyBase.Text.Remove(MyBase.SelectionStart, MyBase.SelectionLength) Else If MyBase.SelectionStart > 0 Then newText = MyBase.Text.Remove(MyBase.SelectionStart - 1, 1) End If End If showHidePrompt(oldText, newText) End If MyBase.OnKeyUp(e) End Sub Protected Overrides Sub OnKeyPress(ByVal e As System.Windows.Forms.KeyPressEventArgs) If Not Char.IsControl(e.KeyChar) Then 'check if prompt should be hidden Dim newText As String If MyBase.SelectedText <> "" Then newText = MyBase.Text.Replace(prompt, "").Replace(MyBase.SelectedText, "").Insert(MyBase.SelectionStart, e.KeyChar) Else newText = MyBase.Text.Replace(prompt, "").Insert(MyBase.SelectionStart, e.KeyChar) End If showHidePrompt(MyBase.Text, newText) 'if control is being used as an integer textbox... If isInteger Then 'disallow non integer input e.Handled = If(newText = "", True, Not Integer.TryParse(newText, _score)) 'set score property _score = If(MyBase.Text = "", 0, score) End If End If MyBase.OnKeyPress(e) End Sub Protected Overrides Sub OnMouseDown(ByVal e As System.Windows.Forms.MouseEventArgs) 'select all text if text = prompt If MyBase.Text = prompt Then MyBase.SelectAll() MyBase.OnMouseDown(e) End Sub Protected Overrides Sub OnLeave(ByVal e As System.EventArgs) 'check if prompt should be shown showHidePrompt(MyBase.Text, MyBase.Text) MyBase.OnLeave(e) End Sub Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message) 'don't allow pasting If m.Msg = WM_PASTE Then Return MyBase.WndProc(m) End Sub Private Sub showHidePrompt(ByVal previousText As String, ByVal newText As String) 'show or hide the prompt depending on the previous text in the control + the new text in the control 'when the control shows prompt it changes forecolor + selects all text 'when the prompt is hidden it changes forecolor to Color.Black If previousText = prompt And Not newText = prompt Then MyBase.ForeColor = Color.Black Else If newText = "" Then MyBase.Text = prompt MyBase.ForeColor = SystemColors.ControlDark MyBase.SelectAll() End If End If End Sub Public Sub init() 'shows prompt showHidePrompt(MyBase.Text, MyBase.Text) End Sub ''' <summary> ''' prompt property ''' </summary> ''' <remarks>what is displayed as a prompt</remarks> Private _prompt As String Public Property prompt() As String Get Return _prompt End Get Set(ByVal value As String) _prompt = value End Set End Property ''' <summary> ''' score property ''' </summary> ''' <remarks>used when control is used as an integer textbox</remarks> Private _score As Integer Public ReadOnly Property score() As Integer Get Return _score End Get End Property ''' <summary> ''' isInteger property ''' </summary> ''' <remarks>determines whether to allow only integers or any text</remarks> Private _isInteger As Boolean Public Property isInteger() As Boolean Get Return _isInteger End Get Set(ByVal value As Boolean) _isInteger = value End Set End Property End Class
You can download the example Project here: Grades Calculator.zip
I've changed the Form's + all of the Control's Name from the default names.
I've also changed these additional Properties for the Form:
The UserControl has 3 promptTextboxes (although you can use any Controls). For the promptTextboxes (as demonstrated in the last example) you need to set this Property:
This is the main Form code:
Public Class frmDemo Private Sub btnNewRow_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnNewRow.Click 'this adds new dynamic ctrlRow control to the panel Dim cr As New ctrlRow cr.Left = 0 cr.Top = (Me.pnlMain.Controls.OfType(Of ctrlRow).Count * cr.Height) + pnlMain.AutoScrollPosition.Y Me.pnlMain.Controls.Add(cr) End Sub Private Sub btnGetValues_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnGetValues.Click 'this loops through all of the ctrlRow controls on the panel + returns 'their foreName, lastName, + phoneNumber properties For Each cr As ctrlRow In Me.pnlMain.Controls.OfType(Of ctrlRow)() MsgBox(String.Format("ForeName: {0} LastName: {1} Phone Number: {2}", cr.foreName, cr.lastName, cr.phoneNumber)) Next End Sub End Class
This is the UserControl code:
Public Class ctrlRow ''' <summary> ''' foreName property ''' </summary> ''' <remarks>this exposes ptbForeName.Text</remarks> Private _foreName As String Public ReadOnly Property foreName() As String Get Return _foreName End Get End Property ''' <summary> ''' lastName property ''' </summary> ''' <remarks>this exposes ptbLastName.Text</remarks> Private _lastName As String Public ReadOnly Property lastName() As String Get Return _lastName End Get End Property ''' <summary> ''' phoneNumber property ''' </summary> ''' <remarks>this exposes ptbPhone.Text</remarks> Private _phoneNumber As String Public ReadOnly Property phoneNumber() As String Get Return _phoneNumber End Get End Property Private Sub ptbForeName_TextChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ptbForeName.TextChanged 'set foreName property _foreName = If(ptbForeName.Text <> ptbForeName.prompt, ptbForeName.Text, "") End Sub Private Sub ptbLastName_TextChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ptbLastName.TextChanged 'set lastName property _lastName = If(ptbLastName.Text <> ptbLastName.prompt, ptbLastName.Text, "") End Sub Private Sub ptbPhone_TextChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ptbPhone.TextChanged 'set phoneNumber property _phoneNumber = If(ptbPhone.Text <> ptbPhone.prompt, ptbPhone.Text, "") End Sub Public Sub New() ' This call is required by the Windows Form Designer. InitializeComponent() 'initialize the promptTextboxes ptbForeName.init() ptbLastName.init() ptbPhone.init() End Sub End Class
You can download the example Project here: ctrlRow uc.zip
This is a simple example that shows how to intercept + respond to Form Events. Moving the MousePointer towards the Button causes the Button to be moved.
This example uses a Form + 3 Classes, which I haven't mentioned before. Classes are a way to structure your code into manageable reusable Components.
This is the Form code:
Public Class frmDemo Const minimumDistance As Integer = 50 '50 pixels Private Sub Form1_KeyDown(ByVal sender As Object, ByVal e As System.Windows.Forms.KeyEventArgs) Handles Me.KeyDown 'if ESC then close app. If e.KeyCode = Keys.Escape Then Me.Close() End Sub Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load 'this gives the form a first look at the keyboard input Me.KeyPreview = True End Sub Private Sub Form1_MouseMove(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles Me.MouseMove 'if the pousePosition is less than 50 pixels from the centre 'of the button in any direction, the button is relocated Dim buttonCentre As Point = btnCantClick.Location buttonCentre.Offset(btnCantClick.Width \ 2, btnCantClick.Height \ 2) If measurement.lineLength(buttonCentre, e.Location) < minimumDistance Then Dim angleRadians As Single = CSng(Math.PI * (angles.FindAngle(buttonCentre, e.Location)) / 180) 'Calculate X2 and Y2 Dim pointX2 As Integer = CInt(e.Location.X - Math.Sin(angleRadians) * minimumDistance) Dim pointY2 As Integer = CInt(e.Location.Y + Math.Cos(angleRadians) * minimumDistance) Dim newLocation As New Point(pointX2, pointY2) newLocation.Offset(-(btnCantClick.Width \ 2), -(btnCantClick.Height \ 2)) btnCantClick.Location = newLocation End If End Sub Private Sub btnCantClick_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnCantClick.Click MessageBox.Show("Congratulations! You found the loophole...") End Sub End Class
These are the Classes:
The angles Class:
Public Class angles ''' <summary> ''' returns degrees in this coordinate system: ''' N,E,S,W = 0,90,180,270 degrees ''' </summary> ''' <param name="p1">point1</param> ''' <param name="p2">point2</param> ''' <returns></returns> ''' <remarks></remarks> Public Shared Function FindAngle(ByVal p1 As Point, ByVal p2 As Point) As Integer Dim atn1 As Double = Math.Atan(1) Dim dx As Double = p2.X - p1.X Dim dy As Double = p2.Y - p1.Y 'Avoid divide by 0 error If (dx = 0) Then dx = 1.0E-20 'Find arctangent and (orient to 12 o'clock) Dim angle As Double = Math.Atan(dy / dx) + (atn1 * 2) 'Adjust for quadrant If (p2.X < p1.X) Then angle = angle + (atn1 * 4) Return CInt(angle * 45 / atn1) End Function End Class
The measurement Class:
Public Class measurement ''' <summary> ''' lineLength ''' </summary> ''' <param name="p1">point1</param> ''' <param name="p2">point2</param> ''' <returns>length in pixels between p1 and p2</returns> ''' <remarks></remarks> Public Shared Function lineLength(ByVal p1 As Point, ByVal p2 As Point) As Integer Return CInt(Math.Sqrt(Math.Abs(p1.X - p2.X) ^ 2 + Math.Abs(p1.Y - p2.Y) ^ 2)) End Function End Class
The rounding Class:
Public Class rounding ''' <summary> ''' toInteger ''' </summary> ''' <param name="number"></param> ''' <returns>an integer</returns> ''' <remarks>rounds up or down using midpoint rounding</remarks> Public Shared Function toInteger(ByVal number As Double) As Integer Return CInt(Math.Round(number, 0, MidpointRounding.ToEven)) End Function End Class
You can download the example Project here: btnCantClick.zip