How can I extend a DataGridView ComboBox column's edit control by a second tiny button? - vb.net

I insert DataGridViewComboBoxColumn columns in a DataViewGrid. That works fine.
Now, I want the user to be able to not only select one list item, but to "drilldown" into the list that is behind the combobox, allowing him to edit (insert/delete/update) the list.
I think it would be a good idea to display a ".." button right behind the dropdown button of the combobox. Pushing it leads to a dialog where the list can be maintained.
What I am stumbling upon is:
How would I create such a custom combobox? Is ComboBox (which is the base for the combo box that the combo box column creates as its edit control) open enough to accommodate such an additional button? What would be the container of the tiny buttoon -- the ComboBox descendant?
How I would make the grid create and handle such a custom combobox?
I currently try to solve this by subclassing DataGridViewComboBoxColum, using a DataGridViewComboBoxCell descendent in its CellTemplate assignment, and overriding PositionWEditingPanel and PositionEditingControl to manipulate the sizes of the panel and the combobox so I'd have space for the tiny button.
Is that the correct way?
Or would I have to create a DataGridViewColumn descendant which creates a Panel containing a DataGridView combobox edit control and the tiny button? How would I make sure the column keeps care of the combo box so it has the correct items etc?
Maybe I sound confused, but I probably am after weeks of VB code (doh)....

Here's the code I came up with. Only thing that's missing is the button press event handler.
Improvements welcome!
#Region "Custom column, cell and edit control for Combobox-with-a-'..'-Button"
Public Class DataGridViewComboBoxExColumn
Inherits DataGridViewComboBoxColumn
Public Sub New()
MyBase.New()
CellTemplate = New DataGridViewComboBoxExCell()
End Sub
Public Overrides Property CellTemplate As DataGridViewCell
Get
Return MyBase.CellTemplate
End Get
Set(ByVal value As DataGridViewCell)
If (value IsNot Nothing) AndAlso Not value.GetType().IsAssignableFrom(GetType(DataGridViewComboBoxExCell)) Then
Throw New InvalidCastException("Must be a DataGridViewComboBoxExCell")
End If
MyBase.CellTemplate = value
End Set
End Property
End Class
Public Class DataGridViewComboBoxExCell
Inherits DataGridViewComboBoxCell
Dim HostingPanel As Panel
Public Sub New()
MyBase.New()
Dim TheButton As Button
HostingPanel = New Panel
HostingPanel.BorderStyle = BorderStyle.Fixed3D
HostingPanel.Padding = New Padding(0, 0, 0, 0)
HostingPanel.BackColor = Color.FromKnownColor(KnownColor.Control)
'HostingPanel.ForeColor = Color.Red ' Color.FromKnownColor(KnownColor.ButtonFace)
TheButton = New Button
TheButton.Text = ""
TheButton.BackColor = Color.FromKnownColor(KnownColor.ButtonFace)
TheButton.ImageList = DaCorFredProtMainForm.MainImageList
TheButton.ImageKey = "table_edit.png"
TheButton.Dock = DockStyle.Fill
HostingPanel.Controls.Add(TheButton)
End Sub
Public Overrides Sub InitializeEditingControl(ByVal rowIndex As Integer, ByVal initialFormattedValue As Object, ByVal dataGridViewCellStyle As DataGridViewCellStyle)
MyBase.InitializeEditingControl(rowIndex, initialFormattedValue, dataGridViewCellStyle)
If Not Me.DataGridView.EditingPanel.Controls.Contains(HostingPanel) Then ' Should always be true
Me.DataGridView.EditingPanel.Controls.Add(HostingPanel)
End If
End Sub
Public Overrides Sub DetachEditingControl()
If Me.DataGridView.EditingPanel.Controls.Contains(HostingPanel) Then ' Should always be true
Me.DataGridView.EditingPanel.Controls.Remove(HostingPanel)
End If
MyBase.DetachEditingControl()
End Sub
Public Overrides ReadOnly Property EditType As Type
Get
Return MyBase.EditType
End Get
End Property
Public Overrides ReadOnly Property ValueType As Type
Get
Return MyBase.ValueType
End Get
End Property
Public Overrides Function PositionEditingPanel(ByVal cellBounds As System.Drawing.Rectangle, ByVal cellClip As System.Drawing.Rectangle, ByVal cellStyle As System.Windows.Forms.DataGridViewCellStyle, ByVal singleVerticalBorderAdded As Boolean, ByVal singleHorizontalBorderAdded As Boolean, ByVal isFirstDisplayedColumn As Boolean, ByVal isFirstDisplayedRow As Boolean) As System.Drawing.Rectangle
cellBounds.Width += cellBounds.Height
cellClip.Width += cellClip.Height
Return MyBase.PositionEditingPanel(cellBounds, cellClip, cellStyle, singleVerticalBorderAdded, singleHorizontalBorderAdded, isFirstDisplayedColumn, isFirstDisplayedRow)
End Function
Public Overrides Sub PositionEditingControl(ByVal setLocation As Boolean, ByVal setSize As Boolean, ByVal cellBounds As System.Drawing.Rectangle, ByVal cellClip As System.Drawing.Rectangle, ByVal cellStyle As System.Windows.Forms.DataGridViewCellStyle, ByVal singleVerticalBorderAdded As Boolean, ByVal singleHorizontalBorderAdded As Boolean, ByVal isFirstDisplayedColumn As Boolean, ByVal isFirstDisplayedRow As Boolean)
MyBase.PositionEditingControl(setLocation, setSize, cellBounds, cellClip, cellStyle, singleVerticalBorderAdded, singleHorizontalBorderAdded, isFirstDisplayedColumn, isFirstDisplayedRow)
Me.DataGridView.EditingControl.Width -= Me.DataGridView.EditingPanel.Height
HostingPanel.Width = Me.DataGridView.EditingPanel.Height
HostingPanel.Height = Me.DataGridView.EditingPanel.Height
HostingPanel.Location = New Point(DataGridView.EditingPanel.Size.Width - DataGridView.EditingPanel.Size.Height, 0)
End Sub
End Class
#End Region

Related

DataGridView column of arbitrary controls per row (or replicate that appearance)

I have a DataGridView which contains, among other things, a "Feature" column and a "Value" column. The "Feature" column is a DataGridViewComboBoxColumn. What I would like to do, is manipulate the "Value" column, such that it can be one of a number of controls, depending on the item selected in the "Feature" column on any given row. So, for example :
Feature
Value Cell Behaviour
A
Combobox with a pre-determined set of options, specific to Feature A
B
Combobox with a different set of pre-determined options, specific to Feature B
C
Checkbox
D
Textbox (free-format)
X
etc. etc.
My initial naive approach to this (which I never really expected to work but figured I'd try anyway...) was to programmatically manipulate the specific value cell in the grid whenever the Feature combobox on the same row was changed :
Private Sub dgv_CellValueChanged(sender As Object, e As DataGridViewCellEventArgs) Handles dgv.CellValueChanged
Dim changedCell As DataGridViewCell = CType(dgv.Rows(e.RowIndex).Cells(e.ColumnIndex), DataGridViewCell)
If changedCell.ColumnIndex = cboFeatureColumn.Index Then
Dim cboChangedFeature = CType(changedCell, DataGridViewComboBoxCell)
If cboChangedFeature.Value IsNot Nothing Then ConfigureValueField(cboChangedFeature)
End If
End Sub
Private Sub ConfigureValueField(cboFeature As DataGridViewComboBoxCell)
Dim cllValueField As DataGridViewCell = dgv.Rows(cboFeature.RowIndex).Cells(valueColumn.Index)
Dim featureID As Integer = dgv.Rows(cboFeature.RowIndex).Cells(featureIDColumn.Index).Value
Dim matches = From row In dtbFeatureList Let lookupID = row.Field(Of Integer)("ID") Where lookupID = featureID
Dim strFieldControl As String = ""
If matches.Any Then strFeatureControl = matches.First().row.Field(Of String)("FieldControl")
Select Case strFieldControl
Case "Checkbox"
' Do something
Case "Textbox"
' Do something
Case "Combobox"
Dim cboConfigured As New DataGridViewComboBoxCell
Dim dvOptions As New DataView(dtbFeatureValueList)
dvOptions.RowFilter = "[FeatureID] = " & featureID
Dim dtbOptions As DataTable
dtbOptions = dvOptions.ToTable
With cboConfigured
.DataSource = dtbOptions
.ValueMember = "Value"
.DisplayMember = "ValueText"
.DisplayStyle = DataGridViewComboBoxDisplayStyle.ComboBox
.ReadOnly = False
End With
cllValueField = cboConfigured
End Select
End Sub
But this (probably, obviously to many) doesn't work; for starters it throws a DataError Default Error Dialog :
The following exception occurred in the DataGridView:
System.FormatException: DataGridViewComboBoxCell value is not valid To
replace this default dialog please handle the DataError event.
...which I can trap and handle (i.e. ignore!) easily enough :
Private Sub dgv_DataError(sender As Object, e As DataGridViewDataErrorEventArgs) Handles dgv.DataError
e.Cancel = True
End Sub
...and the resulting cell does have the appearance of a combobox but nothing happens when I click the dropdown (no list options appear) I suspect there are a whole host of DataErrors being thrown; to be quite honest, I'm not entirely comfortable with ignoring exceptions like this without handling them properly...
The only alternative option I can think of is to add separate columns for each possible value type (so add a DataGridViewComboBoxColumn for combos, a DataGridViewCheckBoxColumn for checkboxes, a DataGridViewTextBoxColumn for textboxes etc.) but then all of my values are scattered across multiple columns instead of all under a single heading which will be really confusing to look at.
And I really want to retain the appearance of comboboxes (set list of options) versus checkboxes (boolean values) versus textboxes (free-format text), as it makes it a lot easier for users to differentiate the values.
I read somewhere that it may be possible to derive my own custom column class, inheriting the native DataGridViewColumn class, but I couldn't find any examples of how this was done. I've done something similar with a customised DataGridViewButtonColumn but literally just to change the appearance of the buttons across the entire column, not the functionality of the individual cells within it.
Would anybody have any suggestions as to how it might be possible to have a mix of controls in the same column, configured specifically to the row in which they reside?
EDIT
So, I followed the walkthrough at : https://learn.microsoft.com/en-us/dotnet/desktop/winforms/controls/how-to-host-controls-in-windows-forms-datagridview-cells?view=netframeworkdesktop-4.8&redirectedfrom=MSDN
And I've added four new classes to my project as follows :
UserControl class : a basic control which contains a textbox, a combobox and a checkbox, and basic methods for showing/hiding each one as appropriate, configuring the combobox if necessary etc. By default, the UserControl should display as an empty textbox.
CustomConfigurableCellEditingControl : derived from the UserControl in #1
DataGridViewCustomConfigurableCell : container for the EditingControl in #2 and derived from DataGridViewTextBoxCell
DataGridViewCustomConfigurableColumn : container for the Cell in #3 and derived from DataGridViewColumn
Public Class UserControl
Private displayControl As String
Private displayValue As String
Private myValue As String
Public Sub New()
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
displayControl = "Textbox"
displayValue = ""
refreshDisplay()
End Sub
Public Property ControlToDisplay As String
Get
Return displayControl
End Get
Set(value As String)
displayControl = value
End Set
End Property
Public Property ValueToDisplay As String
Get
Return displayValue
End Get
Set(value As String)
displayValue = value
End Set
End Property
Public Property Value As String
Get
Return myValue
End Get
Set(value As String)
myValue = value
End Set
End Property
Public Sub refreshDisplay()
Select Case displayControl
Case "Textbox"
With ucTextBox
.Text = displayValue
.Visible = True
End With
ucComboBox.Visible = False
ucCheckbox.Visible = False
Case "Combobox"
With ucComboBox
.Visible = True
End With
ucTextBox.Visible = False
ucCheckbox.Visible = False
Case "Checkbox"
With ucCheckbox
.Checked = myValue
.Visible = True
End With
ucTextBox.Visible = False
ucComboBox.Visible = False
End Select
End Sub
Public Sub configureCombobox(dtb As DataTable, valueMember As String, displayMember As String, style As ComboBoxStyle)
With ucComboBox
.DataSource = dtb
.ValueMember = "ID"
.DisplayMember = "FriendlyName"
.DropDownStyle = style
End With
End Sub
End Class
Class CustomConfigurableCellEditingControl
Inherits UserControl
Implements IDataGridViewEditingControl
Private dataGridViewControl As DataGridView
Private valueIsChanged As Boolean = False
Private rowIndexNum As Integer
Public Sub New()
End Sub
Public Property EditingControlFormattedValue() As Object _
Implements IDataGridViewEditingControl.EditingControlFormattedValue
Get
'Return Me.Value.ToShortDateString()
Return Me.valueIsChanged.ToString
End Get
Set(ByVal value As Object)
Me.Value = value
End Set
End Property
Public Function GetEditingControlFormattedValue(ByVal context As DataGridViewDataErrorContexts) As Object _
Implements IDataGridViewEditingControl.GetEditingControlFormattedValue
Return Me.valueIsChanged.ToString
End Function
Public Sub ApplyCellStyleToEditingControl(ByVal dataGridViewCellStyle As DataGridViewCellStyle) _
Implements IDataGridViewEditingControl.ApplyCellStyleToEditingControl
Me.Font = dataGridViewCellStyle.Font
End Sub
Public Property EditingControlRowIndex() As Integer _
Implements IDataGridViewEditingControl.EditingControlRowIndex
Get
Return rowIndexNum
End Get
Set(ByVal value As Integer)
rowIndexNum = value
End Set
End Property
Public Function EditingControlWantsInputKey(ByVal key As Keys,
ByVal dataGridViewWantsInputKey As Boolean) As Boolean _
Implements IDataGridViewEditingControl.EditingControlWantsInputKey
End Function
Public Sub PrepareEditingControlForEdit(ByVal selectAll As Boolean) _
Implements IDataGridViewEditingControl.PrepareEditingControlForEdit
' No preparation needs to be done.
End Sub
Public ReadOnly Property RepositionEditingControlOnValueChange() As Boolean _
Implements IDataGridViewEditingControl.RepositionEditingControlOnValueChange
Get
Return False
End Get
End Property
Public Property EditingControlDataGridView() As DataGridView _
Implements IDataGridViewEditingControl.EditingControlDataGridView
Get
Return dataGridViewControl
End Get
Set(ByVal value As DataGridView)
dataGridViewControl = value
End Set
End Property
Public Property EditingControlValueChanged() As Boolean _
Implements IDataGridViewEditingControl.EditingControlValueChanged
Get
Return valueIsChanged
End Get
Set(ByVal value As Boolean)
valueIsChanged = value
End Set
End Property
Public ReadOnly Property EditingControlCursor() As Cursor _
Implements IDataGridViewEditingControl.EditingPanelCursor
Get
Return MyBase.Cursor
End Get
End Property
Protected Overrides Sub OnValueChanged(ByVal eventargs As EventArgs)
' Notify the DataGridView that the contents of the cell have changed.
valueIsChanged = True
Me.EditingControlDataGridView.NotifyCurrentCellDirty(True)
MyBase.OnValueChanged(eventargs)
End Sub
End Class
Public Class DataGridViewCustomConfigurableCell
Inherits DataGridViewTextBoxCell
Public Sub New()
End Sub
Public Overrides Sub InitializeEditingControl(ByVal rowIndex As Integer,
ByVal initialFormattedValue As Object,
ByVal dataGridViewCellStyle As DataGridViewCellStyle)
' Set the value of the editing control to the current cell value.
MyBase.InitializeEditingControl(rowIndex, initialFormattedValue, dataGridViewCellStyle)
Dim ctl As CustomConfigurableCellEditingControl = CType(DataGridView.EditingControl, CustomConfigurableCellEditingControl)
' Use the default row value when Value property is null.
If (Me.Value Is Nothing) Then
ctl.Value = CType(Me.DefaultNewRowValue, String)
Else
ctl.Value = CType(Me.Value, String)
End If
End Sub
Public Overrides ReadOnly Property EditType() As Type
Get
' Return the type of the editing control that Cell uses.
Return GetType(CustomConfigurableCellEditingControl)
End Get
End Property
Public Overrides ReadOnly Property ValueType() As Type
Get
' Return the type of the value that Cell contains.
Return GetType(String)
End Get
End Property
Public Overrides ReadOnly Property DefaultNewRowValue() As Object
Get
Return ""
End Get
End Property
End Class
Imports System.Windows.Forms
Public Class DataGridViewCustomConfigurableColumn
Inherits DataGridViewColumn
Public Sub New()
MyBase.New(New DataGridViewCustomConfigurableCell())
End Sub
Public Overrides Property CellTemplate() As DataGridViewCell
Get
Return MyBase.CellTemplate
End Get
Set(ByVal value As DataGridViewCell)
' Ensure that the cell used for the template is a Custom Configurable Cell.
If (value IsNot Nothing) AndAlso Not value.GetType().IsAssignableFrom(GetType(DataGridViewCustomConfigurableCell)) Then
Throw New InvalidCastException("Must be a Custom Configurable Cell")
End If
MyBase.CellTemplate = value
End Set
End Property
End Class
But... I'm still none the wiser as to how I populate, display, manipulate etc. The code compiles fine, but I just get a blank column. I can't see any controls, I can't see any values and I can't seem to "trap" any of the events that should manipulate them?
With dgv
cfgValueColumn = New DataGridViewCustomConfigurableColumn With {.DisplayIndex = valueColumn.Index + 1}
With cfgValueColumn
.HeaderText = "Custom Value"
.Width = 300
End With
.Columns.Add(cfgValueColumn)
Dim cfgCell As DataGridViewCustomConfigurableCell
For Each row As DataGridViewRow In .Rows
cfgCell = CType(row.Cells(cfgValueColumn.Index), DataGridViewCustomConfigurableCell)
With cfgCell
.Value = row.Cells(valueColumn.Index).Value
End With
Next
End With
Your main approach is correct if you just need to change the type of the given cell based on the ComboBox selection.
When the value changes of the ComboBox cell:
Dispose of the current cell of the target column.
Create a new cell whose type is defined by the ComboBox selection.
Assign the new cell to the grid place of the old one.
Here's a working example.
Private Sub dgv_CellValueChanged(
sender As Object,
e As DataGridViewCellEventArgs) Handles dgv.CellValueChanged
If e.RowIndex < 0 Then Return
If e.ColumnIndex = dgvcTypeSelector.Index Then
Dim cmb = DirectCast(dgv(e.ColumnIndex, e.RowIndex), DataGridViewComboBoxCell)
Dim selItem = cmb.FormattedValue.ToString()
Dim valSelCell = dgv(dgvcValueSelector.Index, e.RowIndex)
valSelCell.Dispose()
Select Case selItem
Case "Text"
valSelCell = New DataGridViewTextBoxCell With {
.Value = "Initial value If any."
}
Case "Combo"
valSelCell = New DataGridViewComboBoxCell With {
.ValueMember = "Value",
.DisplayMember = "ValueText",
.DataSource = dt,
.Value = 2 ' Optional...
}
Case "Check"
valSelCell = New DataGridViewCheckBoxCell With {
.ValueType = GetType(String),
.Value = True
}
valSelCell.Style.Alignment = DataGridViewContentAlignment.MiddleCenter
Case "Button"
valSelCell = New DataGridViewButtonCell With {
.Value = "Click Me!"
}
valSelCell.Style.Alignment = DataGridViewContentAlignment.MiddleCenter
End Select
dgv(dgvcValueSelector.Index, e.RowIndex) = valSelCell
ElseIf e.ColumnIndex = dgvcValueSelector.Index Then
Dim cell = dgv(e.ColumnIndex, e.RowIndex)
Console.WriteLine($"{cell.Value} - {cell.FormattedValue}")
End If
End Sub
Private Sub dgv_CurrentCellDirtyStateChanged(
sender As Object,
e As EventArgs) Handles dgv.CurrentCellDirtyStateChanged
If dgv.IsCurrentCellDirty Then
dgv.CommitEdit(DataGridViewDataErrorContexts.Commit)
End If
End Sub
' If you need to handle the Button cells.
Private Sub dgv_CellContentClick(
sender As Object,
e As DataGridViewCellEventArgs) Handles dgv.CellContentClick
If e.RowIndex >= 0 AndAlso
e.ColumnIndex = dgvcValueSelector.Index Then
Dim btn = TryCast(dgv(e.ColumnIndex, e.RowIndex), DataGridViewButtonCell)
If btn IsNot Nothing Then
Console.WriteLine(btn.FormattedValue)
End If
End If
End Sub
Note, I've changed the value type of the check box cell by ValueType = GetType(String) to avoid throwing exceptions caused by the different value types of the check box and main columns. I'd assume the main column here is of type DataGridViewTextBoxColumn. So you have String vs. Boolean types. If you face problems in the data binding scenarios, then just swallow the exception.
Private Sub dgv_DataError(
sender As Object,
e As DataGridViewDataErrorEventArgs) Handles dgv.DataError
If e.RowIndex >= 0 AndAlso e.ColumnIndex = dgvcValueSelector.Index Then
e.ThrowException = False
End If
End Sub

CheckBox not work properly when clicked on quickly multiple times

What this code do:
If parent node is checked/unchecked, also check/uncheck all child nodes.
If just one child node is checked, also check parent node.
Private Sub TreeView1_AfterCheck(sender As Object, e As TreeViewEventArgs) Handles TreeView1.AfterCheck
If updatingTreeView Then Return
updatingTreeView = True
CheckNode(e.Node, e.Node.Checked)
HasCheckedChildNode = 0
updatingTreeView = False
End Sub
Private Sub CheckNode(node As TreeNode, isChecked As Boolean)
If node.Parent IsNot Nothing Then
HasCheckedNode(node.Parent)
If Not isChecked And HasCheckedChildNode > 0 Then Return
node.Parent.Checked = isChecked
ElseIf node.Parent Is Nothing Then
For Each cn As TreeNode In node.Nodes
cn.Checked = isChecked
Next
End If
End Sub
Private Sub HasCheckedNode(node As TreeNode)
For Each cn As TreeNode In node.Nodes
If cn.Checked = True Then
HasCheckedChildNode += 1
ElseIf cn.Checked = False Then
HasCheckedChildNode -= 0
End If
Next
End Sub
This code works fine.
Problem:
When I clicks quickly some of the checkboxes are checked and some no
E.g. Sometimes I checked the parent node but all child nodes still remain unchecked. Sometimes the parent node is unchecked but its child nodes still checked.
Please check the example image:
How to solve this, is this a problem with my PC?
That happens because the TreeView by default doesn't toggle the Check property of the TreeNode objects on mouse double click over the check box area. You need to intercept the WM_LBUTTONDBLCLK messages, get TreeViewHitTestInfo of the double clicked point, and toggle the Check property if the double clicked point is over the check box.
Here's a custom TreeView for that, also it solves the main issue, checking/unchecking the parent and child nodes of the branch, just enable the AutoCheckParents and/or AutoCheckChildren properties for that.
Imports System.ComponentModel
Imports System.Runtime.InteropServices
Imports System.Windows.Forms
<DesignerCategory("Code")>
Public Class DoubleClickCheckTreeView
Inherits TreeView
#Region "Properties"
<Category("Behavior"),
DefaultValue(False)>
Public Property AutoCheckParents As Boolean = False
<Category("Behavior"),
DefaultValue(False)>
Public Property AutoCheckChildren As Boolean = False
#End Region
#Region "Overrides"
'Enable DoubleBuffered to reduce the flickering.
Protected Overrides Sub OnHandleCreated(e As EventArgs)
SendMessage(Handle,
TVM_SETEXTENDEDSTYLE,
IntPtr.op_Explicit(TVS_EX_DOUBLEBUFFER),
IntPtr.op_Explicit(TVS_EX_DOUBLEBUFFER))
MyBase.OnHandleCreated(e)
End Sub
Protected Overrides Sub WndProc(ByRef m As Message)
If m.Msg = WM_LBUTTONDBLCLK AndAlso CheckBoxes Then
Dim x As Integer = m.LParam.ToInt32() And &HFFFF
Dim y As Integer = (m.LParam.ToInt32 >> 16) And &HFFFF
Dim ht As TreeViewHitTestInfo = HitTest(x, y)
If ht.Node IsNot Nothing AndAlso
ht.Location = TreeViewHitTestLocations.StateImage Then
OnBeforeCheck(New TreeViewCancelEventArgs(ht.Node,
False,
TreeViewAction.ByMouse))
ht.Node.Checked = Not ht.Node.Checked
OnAfterCheck(New TreeViewEventArgs(ht.Node, TreeViewAction.ByMouse))
m.Result = IntPtr.Zero
Return
End If
End If
MyBase.WndProc(m)
End Sub
Protected Overrides Sub OnAfterCheck(e As TreeViewEventArgs)
MyBase.OnAfterCheck(e)
If e.Action = TreeViewAction.Unknown OrElse
Not CheckBoxes Then Return
If AutoCheckParents Then
Dim p = e.Node.Parent
While p IsNot Nothing
p.Checked = p.Nodes.Cast(Of TreeNode).Any(Function(x) x.Checked)
p = p.Parent
End While
End If
If AutoCheckChildren Then
For Each tn As TreeNode In GetNodes(e.Node)
tn.Checked = e.Node.Checked
Next
End If
End Sub
#End Region
#Region "Private Methods"
Private Iterator Function GetNodes(node As TreeNode) As IEnumerable(Of TreeNode)
For Each n As TreeNode In node.Nodes
Yield n
For Each c As TreeNode In GetNodes(n)
Yield c
Next
Next
End Function
#End Region
#Region "API"
Private Const TVM_SETEXTENDEDSTYLE As Integer = &H1100 + 44
Private Const TVM_GETEXTENDEDSTYLE As Integer = &H1100 + 45
Private Const TVS_EX_DOUBLEBUFFER As Integer = &H4
Private Const WM_LBUTTONDBLCLK As Integer = &H203
<DllImport("user32.dll")>
Private Shared Function SendMessage(ByVal hWnd As IntPtr,
ByVal msg As Integer,
ByVal wp As IntPtr,
ByVal lp As IntPtr) As IntPtr
End Function
#End Region
End Class
Add a new class to your project and paste this code.
Rebuild.
Drop an instance of the DoubleClickCheckTreeView or change the type of the existing default TreeView in the Designer.
Related
◉ AfterCheck and AfterSelect events in TreeView, Windows Forms (c#)
I would imagine you would have to handle your double click event when its fired, and I suspect your code for that is going to look very similar to your single click event (but who knows)

Radio button on the ContextMenu or ContextMenuStrip [winforms]

I am searching for the way to implement a context menu item with radio button appearance like this: Windows 7's ContextMenu with RadioButton menu item
I have search through Google and SO, this post Adding RadioButtons to ContextMenu is close, but it's related to Java, and I am looking for a control or renderer in .NET for winforms.
Any solution or suggestion would be great help. Thank you.
Option buttons, also known as radio buttons, are similar to check boxes except that users can select only one at a time. Although by default the ToolStripMenuItem class does not provide option-button behavior, the class does provide check-box behavior that you can customize to implement option-button behavior for menu items in a MenuStrip control.
When the CheckOnClick property of a menu item is true, users can click the item to toggle the display of a check mark. The Checked property indicates the current state of the item. To implement basic option-button behavior, you must ensure that when an item is selected, you set the Checked property for the previously selected item to false.
The following procedures describe how to implement this and additional functionality in a class that inherits the ToolStripMenuItem class. The ToolStripRadioButtonMenuItem class overrides members such as OnCheckedChanged and OnPaint to provide the selection behavior and appearance of option buttons. Additionally, this class overrides the Enabled property so that options on a submenu are disabled unless the parent item is selected.
First Create a Class for RadioButton
This is combination of RadioButton and ToggleButton.
Public Class ToolStripRadioButtonMenuItem
Inherits ToolStripMenuItem
Public Sub New()
MyBase.New()
Initialize()
End Sub
Public Sub New(ByVal text As String)
MyBase.New(text, Nothing, CType(Nothing, EventHandler))
Initialize()
End Sub
Public Sub New(ByVal image As Image)
MyBase.New(Nothing, image, CType(Nothing, EventHandler))
Initialize()
End Sub
Public Sub New(ByVal text As String, ByVal image As Image)
MyBase.New(text, image, CType(Nothing, EventHandler))
Initialize()
End Sub
Public Sub New(ByVal text As String, _
ByVal image As Image, ByVal onClick As EventHandler)
MyBase.New(text, image, onClick)
Initialize()
End Sub
Public Sub New(ByVal text As String, ByVal image As Image, _
ByVal onClick As EventHandler, ByVal name As String)
MyBase.New(text, image, onClick, name)
Initialize()
End Sub
Public Sub New(ByVal text As String, ByVal image As Image, _
ByVal ParamArray dropDownItems() As ToolStripItem)
MyBase.New(text, image, dropDownItems)
Initialize()
End Sub
Public Sub New(ByVal text As String, ByVal image As Image, _
ByVal onClick As EventHandler, ByVal shortcutKeys As Keys)
MyBase.New(text, image, onClick)
Initialize()
Me.ShortcutKeys = shortcutKeys
End Sub
' Called by all constructors to initialize CheckOnClick.
Private Sub Initialize()
CheckOnClick = True
End Sub
Protected Overrides Sub OnCheckedChanged(ByVal e As EventArgs)
MyBase.OnCheckedChanged(e)
' If this item is no longer in the checked state, do nothing.
If Not Checked Then Return
' Clear the checked state for all siblings.
For Each item As ToolStripItem In Parent.Items
Dim radioItem As ToolStripRadioButtonMenuItem = _
TryCast(item, ToolStripRadioButtonMenuItem)
If radioItem IsNot Nothing AndAlso _
radioItem IsNot Me AndAlso _
radioItem.Checked Then
radioItem.Checked = False
' Only one item can be selected at a time,
' so there is no need to continue.
Return
End If
Next
End Sub
Protected Overrides Sub OnClick(ByVal e As EventArgs)
' If the item is already in the checked state, do not call
' the base method, which would toggle the value.
If Checked Then Return
MyBase.OnClick(e)
End Sub
' Let the item paint itself, and then paint the RadioButton
' where the check mark is displayed, covering the check mark
' if it is present.
Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)
MyBase.OnPaint(e)
' If the client sets the Image property, the selection behavior
' remains unchanged, but the RadioButton is not displayed and the
' selection is indicated only by the selection rectangle.
If Image IsNot Nothing Then Return
' Determine the correct state of the RadioButton.
Dim buttonState As RadioButtonState = RadioButtonState.UncheckedNormal
If Enabled Then
If mouseDownState Then
If Checked Then
buttonState = RadioButtonState.CheckedPressed
Else
buttonState = RadioButtonState.UncheckedPressed
End If
ElseIf mouseHoverState Then
If Checked Then
buttonState = RadioButtonState.CheckedHot
Else
buttonState = RadioButtonState.UncheckedHot
End If
Else
If Checked Then buttonState = RadioButtonState.CheckedNormal
End If
Else
If Checked Then
buttonState = RadioButtonState.CheckedDisabled
Else
buttonState = RadioButtonState.UncheckedDisabled
End If
End If
' Calculate the position at which to display the RadioButton.
Dim offset As Int32 = CInt((ContentRectangle.Height - _
RadioButtonRenderer.GetGlyphSize( _
e.Graphics, buttonState).Height) / 2)
Dim imageLocation As Point = New Point( _
ContentRectangle.Location.X + 4, _
ContentRectangle.Location.Y + offset)
' If the item is selected and the RadioButton paints with partial
' transparency, such as when theming is enabled, the check mark
' shows through the RadioButton image. In this case, paint a
' non-transparent background first to cover the check mark.
If Checked AndAlso RadioButtonRenderer _
.IsBackgroundPartiallyTransparent(buttonState) Then
Dim glyphSize As Size = RadioButtonRenderer _
.GetGlyphSize(e.Graphics, buttonState)
glyphSize.Height -= 1
glyphSize.Width -= 1
Dim backgroundRectangle As _
New Rectangle(imageLocation, glyphSize)
e.Graphics.FillEllipse( _
SystemBrushes.Control, backgroundRectangle)
End If
RadioButtonRenderer.DrawRadioButton( _
e.Graphics, imageLocation, buttonState)
End Sub
Private mouseHoverState As Boolean = False
Protected Overrides Sub OnMouseEnter(ByVal e As EventArgs)
mouseHoverState = True
' Force the item to repaint with the new RadioButton state.
Invalidate()
MyBase.OnMouseEnter(e)
End Sub
Protected Overrides Sub OnMouseLeave(ByVal e As EventArgs)
mouseHoverState = False
MyBase.OnMouseLeave(e)
End Sub
Private mouseDownState As Boolean = False
Protected Overrides Sub OnMouseDown(ByVal e As MouseEventArgs)
mouseDownState = True
' Force the item to repaint with the new RadioButton state.
Invalidate()
MyBase.OnMouseDown(e)
End Sub
Protected Overrides Sub OnMouseUp(ByVal e As MouseEventArgs)
mouseDownState = False
MyBase.OnMouseUp(e)
End Sub
' Enable the item only if its parent item is in the checked state
' and its Enabled property has not been explicitly set to false.
Public Overrides Property Enabled() As Boolean
Get
Dim ownerMenuItem As ToolStripMenuItem = _
TryCast(OwnerItem, ToolStripMenuItem)
' Use the base value in design mode to prevent the designer
' from setting the base value to the calculated value.
If Not DesignMode AndAlso _
ownerMenuItem IsNot Nothing AndAlso _
ownerMenuItem.CheckOnClick Then
Return MyBase.Enabled AndAlso ownerMenuItem.Checked
Else
Return MyBase.Enabled
End If
End Get
Set(ByVal value As Boolean)
MyBase.Enabled = value
End Set
End Property
' When OwnerItem becomes available, if it is a ToolStripMenuItem
' with a CheckOnClick property value of true, subscribe to its
' CheckedChanged event.
Protected Overrides Sub OnOwnerChanged(ByVal e As EventArgs)
Dim ownerMenuItem As ToolStripMenuItem = _
TryCast(OwnerItem, ToolStripMenuItem)
If ownerMenuItem IsNot Nothing AndAlso _
ownerMenuItem.CheckOnClick Then
AddHandler ownerMenuItem.CheckedChanged, New _
EventHandler(AddressOf OwnerMenuItem_CheckedChanged)
End If
MyBase.OnOwnerChanged(e)
End Sub
' When the checked state of the parent item changes,
' repaint the item so that the new Enabled state is displayed.
Private Sub OwnerMenuItem_CheckedChanged( _
ByVal sender As Object, ByVal e As EventArgs)
Invalidate()
End Sub
End Class
Second Create a Class of Form1
Public Class Form1
Inherits Form
Private sample As New MenuStrip()
Private mainToolStripMenuItem As New ToolStripMenuItem()
Private toolStripMenuItem1 As New ToolStripMenuItem()
Private toolStripRadioButtonMenuItem1 As New ToolStripRadioButtonMenuItem()
Private toolStripRadioButtonMenuItem2 As New ToolStripRadioButtonMenuItem()
Private toolStripRadioButtonMenuItem3 As New ToolStripRadioButtonMenuItem()
Private toolStripRadioButtonMenuItem4 As New ToolStripRadioButtonMenuItem()
Private toolStripRadioButtonMenuItem5 As New ToolStripRadioButtonMenuItem()
Private toolStripRadioButtonMenuItem6 As New ToolStripRadioButtonMenuItem()
Public Sub New()
Me.mainToolStripMenuItem.Text = "main"
toolStripRadioButtonMenuItem1.Text = "option 1"
toolStripRadioButtonMenuItem2.Text = "option 2"
toolStripRadioButtonMenuItem3.Text = "option 2-1"
toolStripRadioButtonMenuItem4.Text = "option 2-2"
toolStripRadioButtonMenuItem5.Text = "option 3-1"
toolStripRadioButtonMenuItem6.Text = "option 3-2"
toolStripMenuItem1.Text = "toggle"
toolStripMenuItem1.CheckOnClick = True
mainToolStripMenuItem.DropDownItems.AddRange(New ToolStripItem() { _
toolStripRadioButtonMenuItem1, toolStripRadioButtonMenuItem2, _
toolStripMenuItem1})
toolStripRadioButtonMenuItem2.DropDownItems.AddRange( _
New ToolStripItem() {toolStripRadioButtonMenuItem3, _
toolStripRadioButtonMenuItem4})
toolStripMenuItem1.DropDownItems.AddRange(New ToolStripItem() { _
toolStripRadioButtonMenuItem5, toolStripRadioButtonMenuItem6})
sample.Items.AddRange(New ToolStripItem() {mainToolStripMenuItem})
Controls.Add(sample)
MainMenuStrip = sample
Text = "ToolStripRadioButtonMenuItem demo"
End Sub
End Class
Last is Create a Class for Program
Public Class Program
<STAThread()> Public Shared Sub Main()
Application.EnableVisualStyles()
Application.SetCompatibleTextRenderingDefault(False)
Application.Run(New Form1())
End Sub
End Class
Screenshot
.
Credits to Karl Erickson and to his blog about RadioButton and Menustrip.
Check his blog here.

Hiding up/down buttons on NumericUpDown control

I am trying to subclass NumericUpDown in several ways to get better functionality and appearance.
Since NUD is construct of two controls I would like to hide up/down buttons in cases where property "Increment" is set to 0.
This code is in subclass:
Protected Overrides Sub OnTextBoxResize(ByVal source As Object, ByVal e As System.EventArgs)
Controls(0).Hide()
End Sub
... and it work OK.
But in that function I cannot check a value of Increment property like this:
Protected Overrides Sub OnTextBoxResize(ByVal source As Object, ByVal e As System.EventArgs)
If Me.Increment = 0 Then
Controls(0).Hide()
End if
End Sub
In scope of this function Me is not reachable.
I am also try with using local variables but can't find which event is fired before OnTextBoxResize to read value of Increment property.
What to do in such case to get desired functionality?
This seems to work fairly well. It Shadows the Increment property in order to set the visibility of the spinner controls when the Increment value is being changed. There is an underlying private method the base control calls called PositionControls which you cannot stop — that method could create some flicker, but on my test, it didn't.
Public Class MyNumBox
Inherits NumericUpDown
Shadows Property Increment As Decimal
Get
Return MyBase.Increment
End Get
Set(value As Decimal)
MyBase.Increment = value
OnTextBoxResize(Me, EventArgs.Empty)
End Set
End Property
Protected Overrides Sub OnHandleCreated(e As EventArgs)
MyBase.OnHandleCreated(e)
OnTextBoxResize(Me, EventArgs.Empty)
End Sub
Protected Overrides Sub OnTextBoxResize(source As Object, e As EventArgs)
If Me.IsHandleCreated Then
Me.Height = Me.PreferredHeight
Me.Controls(0).Visible = (MyBase.Increment > 0)
Dim borderWidth As Integer = 0
If Me.BorderStyle > BorderStyle.None Then
borderWidth = SystemInformation.Border3DSize.Width
End If
Dim textWidth As Integer
If Me.Increment = 0 Then
textWidth = Me.ClientSize.Width - (borderWidth * 2)
Else
textWidth = Me.ClientSize.Width - Me.Controls(0).Width - (borderWidth * 2)
End If
If Me.UpDownAlign = LeftRightAlignment.Left Then
If Me.Increment = 0 Then
Me.Controls(1).SetBounds(borderWidth, borderWidth, _
textWidth, Me.Controls(1).Height)
Else
Me.Controls(1).SetBounds(borderWidth + Me.Controls(0).Width, _
Me.Controls(1).Top, textWidth, Me.Controls(1).Height)
End If
Else
Me.Controls(1).SetBounds(borderWidth, Me.Controls(1).Top, _
textWidth, Me.Controls(1).Height)
End If
Me.Refresh()
End If
End Sub
End Class
In the OnTextBoxResize override, I am re-positioning the controls into their proper place, and this version does account for the UpDownAlign property.
If you can, read this thread over at EE where I answered a similar question. It resizes the edit portion so that the control is redrawn correctly when the buttons have been hidden and the control is resized. *Otherwise the portion of the control where the buttons used to be leaves ghosts behind.
One solution to your specific problem is to wait for the VisibleChanged() event and check the Increment() property from there. Here is a conversion of my previous answer with some minor changes:
Public Class NoArrowNumericUpDown
Inherits NumericUpDown
Private itb As InnerTextBox = Nothing
Protected Overrides Sub OnVisibleChanged(e As System.EventArgs)
If Me.Visible Then
If Me.Increment = 0 AndAlso IsNothing(itb) Then
Dim ctl As Control = Me.Controls(0) ' get the spinners
Me.Controls.Remove(ctl) ' remove the spinners
ctl = Me.Controls(0) ' get the edit control
itb = New InnerTextBox(Me, ctl)
End If
End If
MyBase.OnVisibleChanged(e)
End Sub
Public Class InnerTextBox
Inherits NativeWindow
Private parentControl As Control = Nothing
Const WM_WINDOWPOSCHANGING As Integer = &H46
Public Sub New(parentControl As Control, InnerTextBox As Control)
Me.parentControl = parentControl
Me.AssignHandle(InnerTextBox.Handle)
End Sub
Protected Overrides Sub WndProc(ByRef m As Message)
Select Case m.Msg
Case WM_WINDOWPOSCHANGING
Dim wp As WindowPos = CType(System.Runtime.InteropServices.Marshal.PtrToStructure(m.LParam, GetType(WindowPos)), WindowPos)
If Me.parentControl IsNot Nothing Then
wp.cx = Me.parentControl.ClientSize.Width - 2 * wp.x
wp.cy = Me.parentControl.ClientSize.Height
System.Runtime.InteropServices.Marshal.StructureToPtr(wp, m.LParam, True)
End If
Exit Select
End Select
MyBase.WndProc(m)
End Sub
Public Structure WindowPos
Public hwnd As IntPtr
Public hwndInsertAfter As IntPtr
Public x As Integer
Public y As Integer
Public cx As Integer
Public cy As Integer
Public flags As UInteger
End Structure
End Class
End Class
EDIT: You could just enclose your code in a Try/Catch block?
Public Class NoArrowNumericUpDown
Inherits NumericUpDown
Protected Overrides Sub OnTextBoxResize(ByVal source As Object, ByVal e As System.EventArgs)
Try
If Me.Increment = 0 Then
Controls(0).Hide()
End If
Catch ex As Exception
End Try
End Sub
End Class

Binding Combobox to Object Datasource

I've got a form bound to an object datasource. It has one text box and one combo box. I set up one binding source for the main object and one binding source for the combo box. When I run the form, the text box is bound correctly, and the list of values in the combo box is bound correctly, but the ValueMember of the combo box isn't working correctly.
The combo box shows the correct list, but it's selected index is 0 instead of what it should be 2. When I change the value in the text box, it's bound object's Property.Set method is called correctly, but the same Property.Set method is not called for the combo box.
I know I can hack up the OnSelectedIndex change methods in the form, but I would like to know what I am doing wrong in just using the Bindings.
Here is the code on the form:
Public Class Form1
Private Sub Form1_Load(sender As Object, e As System.EventArgs) Handles Me.Load
Dim NameValueBindingSource1 As New BindingSource()
Dim WorkOrderBindingSource1 As New BindingSource
'Create main object to bind to
Dim wo As New WorkOrder
wo.WOIndex = "2012-0111"
wo.WorkOrderType = 3
'Create list object for combo box
Dim NameValues As BindingList(Of NameValue)
NameValues = FillNameValueList()
'Bind Text Box to Binding Source
WorkOrderBindingSource1.DataSource = wo
WOIndexTextBox1.DataBindings.Add("Text", WorkOrderBindingSource1, "WOIndex")
'Bind Combo Box to Binding Source
NameValueBindingSource1.DataSource = NameValues
WorkOrderTypeCombo.DataSource = NameValueBindingSource1
WorkOrderTypeCombo.DisplayMember = "Value"
WorkOrderTypeCombo.ValueMember = "Code"
End Sub
Function FillNameValueList() As BindingList(Of NameValue)
Dim bl As New BindingList(Of NameValue)
Dim nv As NameValue
nv = New NameValue
bl.Add(New NameValue("Short", 0))
bl.Add(New NameValue("Middle", 1))
bl.Add(New NameValue("Long", 2))
bl.Add(New NameValue("Very Long", 3))
Return bl
End Function
End Class
Here's the code for the main object - "WorkOrder"
Imports System.ComponentModel
Public Class WorkOrder
Implements IEditableObject
Implements INotifyPropertyChanged
Private mWOIndex As String
Private mWorkOrderType As Integer
Public Event PropertyChanged(sender As Object, e As System.ComponentModel.PropertyChangedEventArgs) Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged
Public Property WOIndex As String
Get
Return mWOIndex
End Get
Set(value As String)
mWOIndex = value
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs("WOIndex"))
End Set
End Property
Public Property WorkOrderType As Integer
Get
Return mWorkOrderType
End Get
Set(value As Integer)
mWorkOrderType = value
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs("WorkOrderType"))
End Set
End Property
Public Sub BeginEdit() Implements System.ComponentModel.IEditableObject.BeginEdit
End Sub
Public Sub CancelEdit() Implements System.ComponentModel.IEditableObject.CancelEdit
End Sub
Public Sub EndEdit() Implements System.ComponentModel.IEditableObject.EndEdit
End Sub
End Class
Here's the code for the object used in the combo box
Imports System.ComponentModel
Public Class NameValue
Implements IEditableObject
Implements INotifyPropertyChanged
Private mValue As String
Private mCode As Integer
Public Event PropertyChanged(sender As Object, e As System.ComponentModel.PropertyChangedEventArgs) Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged
Public Property Code As Integer
Get
Return mCode
End Get
Set(value As Integer)
mCode = value
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs("Code"))
End Set
End Property
Public Property Value As String
Get
Return mValue
End Get
Set(value As String)
mValue = value
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs("Value"))
End Set
End Property
Public Sub BeginEdit() Implements System.ComponentModel.IEditableObject.BeginEdit
End Sub
Public Sub CancelEdit() Implements System.ComponentModel.IEditableObject.CancelEdit
End Sub
Public Sub EndEdit() Implements System.ComponentModel.IEditableObject.EndEdit
End Sub
Public Sub New(InitValue As String, InitCode As Integer)
Value = InitValue
Code = InitCode
End Sub
End Class
In your code, you are merely assigning the DataSource to the ComboBox, but you're not establishing any DataBinding for it.
You need a line like this (using C# here):
WorkOrderTypeCombo.DataBindings.Add(new System.Windows.Forms.Binding("SelectedValue", WorkOrderBindingSource1, "WorkOrderType", true));
Hope this helps