问题
I have a case where I would like TreeView to be able to show radio buttons on multiple root nodes, and checkboxes on their children. There would only be one level of children beneath any root node.
The radios should also behave like a group, ie one root is selected and the others' radios deselect.
I've been trying to fake it with images, but it doesn't look realistic. I originally had a listbox and a separate checkedlistbox, but the usability gods struck it down.
Has anyone implemented this functionality or have another suggestion?
Think of it this way:
(o) McDonalds
[ ] Burger
[ ] Fries
[ ] Drink
(o) Burger King
[ ] Burger
[ ] Fries
[ ] Drink
(*) Wendy's
[x] Burger
[x] Fries
[ ] Drink
You can have one big option, but select 1..n underneath the big option.
回答1:
I came up with a solution based on the article http://www.codeproject.com/KB/combobox/RadioListBoxDotNetVersion.aspx. My implementation inherits from CheckedListBox and includes the following methods:
protected override void OnDrawItem (DrawItemEventArgs e)
{
// Erase all background if control has no items
if (e.Index < 0 || e.Index > this.Items.Count - 1)
{
e.Graphics.FillRectangle(new SolidBrush(this.BackColor), this.ClientRectangle);
return;
}
// Calculate bounds for background, if last item paint up to bottom of control
Rectangle rectBack = e.Bounds;
if (e.Index == this.Items.Count - 1)
rectBack.Height = this.ClientRectangle.Top + this.ClientRectangle.Height - e.Bounds.Top;
e.Graphics.FillRectangle(new SolidBrush(this.BackColor), rectBack);
// Determines text color/brush
Brush brushText = SystemBrushes.FromSystemColor(this.ForeColor);
if ((e.State & DrawItemState.Disabled) == DrawItemState.Disabled || (e.State & DrawItemState.Grayed) == DrawItemState.Grayed)
brushText = SystemBrushes.GrayText;
Boolean bIsChecked = this.GetItemChecked(e.Index);
String strText;
if (!string.IsNullOrEmpty(DisplayMember)) // Bound Datatable? Then show the column written in Displaymember
strText = ((System.Data.DataRowView) this.Items[e.Index])[this.DisplayMember].ToString();
else
strText = this.Items[e.Index].ToString();
Size sizeGlyph;
Point ptGlyph;
if (this.GetItemType(e.Index) == ItemType.Radio)
{
RadioButtonState stateRadio = bIsChecked ? RadioButtonState.CheckedNormal : RadioButtonState.UncheckedNormal;
if ((e.State & DrawItemState.Disabled) == DrawItemState.Disabled || (e.State & DrawItemState.Grayed) == DrawItemState.Grayed)
stateRadio = bIsChecked ? RadioButtonState.CheckedDisabled : RadioButtonState.UncheckedDisabled;
// Determines bounds for text and radio button
sizeGlyph = RadioButtonRenderer.GetGlyphSize(e.Graphics, stateRadio);
ptGlyph = e.Bounds.Location;
ptGlyph.X += 4; // a comfortable distance from the edge
ptGlyph.Y += (e.Bounds.Height - sizeGlyph.Height) / 2;
// Draws the radio button
RadioButtonRenderer.DrawRadioButton(e.Graphics, ptGlyph, stateRadio);
}
else
{
CheckBoxState stateCheck = bIsChecked ? CheckBoxState.CheckedNormal : CheckBoxState.UncheckedNormal;
if ((e.State & DrawItemState.Disabled) == DrawItemState.Disabled || (e.State & DrawItemState.Grayed) == DrawItemState.Grayed)
stateCheck = bIsChecked ? CheckBoxState.CheckedDisabled : CheckBoxState.UncheckedDisabled;
// Determines bounds for text and radio button
sizeGlyph = CheckBoxRenderer.GetGlyphSize(e.Graphics, stateCheck);
ptGlyph = e.Bounds.Location;
ptGlyph.X += 20; // a comfortable distance from the edge
ptGlyph.Y += (e.Bounds.Height - sizeGlyph.Height) / 2;
// Draws the radio button
CheckBoxRenderer.DrawCheckBox(e.Graphics, ptGlyph, stateCheck);
}
// Draws the text
Rectangle rectText = new Rectangle(ptGlyph.X + sizeGlyph.Width + 3, e.Bounds.Y, e.Bounds.Width - sizeGlyph.Width, e.Bounds.Height);
e.Graphics.DrawString(strText.Substring(4), e.Font, brushText, rectText, this.oAlign);
// If the ListBox has focus, draw a focus rectangle around the selected item.
e.DrawFocusRectangle();
}
protected override void OnItemCheck (ItemCheckEventArgs ice)
{
base.OnItemCheck(ice);
if (ice.NewValue == CheckState.Unchecked)
return;
if (this.GetItemType(ice.Index) == ItemType.Radio) // if they selected a root, deselect other roots and their children
{
for (Int32 i = 0; i < this.Items.Count; ++i)
{
if (i == ice.Index)
continue;
if (this.GetItemType(i) == ItemType.Radio)
{
this.SetItemChecked(i, false);
Int32 j = i + 1;
while (j < this.Items.Count && this.GetItemType(j) == ItemType.Checkbox)
{
this.SetItemChecked(j, false);
j++;
}
}
}
}
else if (this.GetItemType(ice.Index) == ItemType.Checkbox) // they selected a child; select the root too and deselect other roots and their children
{
// Find parent
Int32 iParentIdx = ice.Index - 1;
while (iParentIdx >= 0 && this.GetItemType(iParentIdx) == ItemType.Checkbox)
iParentIdx--;
this.SetItemChecked(iParentIdx, true);
}
}
protected ItemType GetItemType (Int32 iIdx)
{
String strText = this.Items[iIdx].ToString();
if (strText.StartsWith("(o)"))
return (ItemType.Radio);
else if (strText.StartsWith("[x]"))
return (ItemType.Checkbox);
throw (new Exception("Invalid item type"));
}
来源:https://stackoverflow.com/questions/3375799/winforms-treeview-with-both-radios-and-checkboxes