I am creating a custom treeview control for my winform application. In doing this, I noticed that there is a really annoying feature of the treeview I am having trouble removing.
Upon selecting a node (Mouse Down), the text of that node is highlighted, I don't want this. However, I also don't to use drawing of any kind. I am looking for a way to do this without having to set the style to owner drawn and manually drawing the text of the nodes being added.
public class CustomTreeView : TreeView
{
// Fields for the custom checkbox images
private Image _checkedImage = Properties.Resources.CheckIcon;
private Image _uncheckedImage = Properties.Resources.UnCheckIcon;
private Image _intermediateImage = Properties.Resources.IntermediateCheckIcon;
private ImageList _imageList;
public CustomTreeView()
{
this.CheckBoxes = false;
_imageList = new ImageList();
_imageList.ImageSize = new Size(26, 26);
_imageList.Images.Add("Unchecked", _uncheckedImage);
_imageList.Images.Add("Checked", _checkedImage);
_imageList.Images.Add("Intermediate", _intermediateImage);
this.ImageList = _imageList;
this.Font = new Font("Gadugi", 12f);
this.ForeColor = Color.DodgerBlue;
this.BackColor = SystemColors.Control;
this.BorderStyle = BorderStyle.None;
this.Scrollable = true;
this.ShowLines = false;
this.ShowRootLines = false;
this.ShowNodeToolTips = true;
this.ShowPlusMinus = false;
this.HideSelection = false;
this.ItemHeight = 30;
}
private int GetEffectiveState(TreeNode node)
{
if (node.Nodes.Count == 0)
{
return node.Checked ? 1 : 0;
}
else
{
bool allChecked = true;
bool anyChecked = false;
foreach (TreeNode child in node.Nodes)
{
int childState = GetEffectiveState(child);
if (childState == 0)
allChecked = false;
else if (childState == 1)
anyChecked = true;
else
{
allChecked = false;
anyChecked = true;
}
}
if (allChecked)
return 1;
else if (anyChecked)
return 2;
else
return 0;
}
}
private void UpdateStateImageIndex(TreeNode node)
{
node.ImageIndex = GetEffectiveState(node);
node.SelectedImageIndex = node.ImageIndex;
}
private void SetNodeChecked(TreeNode node, bool checkedState)
{
node.Checked = checkedState;
foreach (TreeNode child in node.Nodes)
{
SetNodeChecked(child, checkedState);
}
}
private void UpdateStateIndices(TreeNode node)
{
while (node != null)
{
UpdateStateImageIndex(node);
node = node.Parent;
}
}
protected override void OnMouseDown(MouseEventArgs e)
{
TreeViewHitTestInfo hitTestInfo = HitTest(e.Location);
if (hitTestInfo.Location == TreeViewHitTestLocations.Image)
{
TreeNode node = hitTestInfo.Node;
if (node != null)
{
int currentState = GetEffectiveState(node);
if (currentState == 1)
{
SetNodeChecked(node, false);
}
else
{
SetNodeChecked(node, true);
}
UpdateStateIndices(node);
}
}
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
TreeViewHitTestInfo hitTestInfo = HitTest(e.Location);
if (hitTestInfo.Location == TreeViewHitTestLocations.Image)
{
this.Cursor = Cursors.Hand;
}
else
{
this.Cursor = Cursors.Default;
}
}
protected override void OnAfterCheck(TreeViewEventArgs e)
{
base.OnAfterCheck(e);
UpdateStateIndices(e.Node);
}
private void UpdateStateIndicesRecursive(TreeNode node)
{
node.Expand();
foreach (TreeNode child in node.Nodes)
{
UpdateStateIndicesRecursive(child);
}
UpdateStateImageIndex(node);
}
public void UpdateAllStateIndices()
{
foreach (TreeNode node in this.Nodes)
{
UpdateStateIndicesRecursive(node);
}
}
protected override void OnBeforeCollapse(TreeViewCancelEventArgs e)
{
e.Cancel = true;
}
protected override void OnKeyDown(KeyEventArgs e)
{
if (e.KeyCode == Keys.Up || e.KeyCode == Keys.Down || e.KeyCode == Keys.Left || e.KeyCode == Keys.Right)
{
e.Handled = true;
}
base.OnKeyDown(e);
}
protected override void OnBeforeSelect(TreeViewCancelEventArgs e)
{
e.Cancel = true;
base.OnBeforeSelect(e);
}
}
I am creating a custom treeview control for my winform application. In doing this, I noticed that there is a really annoying feature of the treeview I am having trouble removing.
Upon selecting a node (Mouse Down), the text of that node is highlighted, I don't want this. However, I also don't to use drawing of any kind. I am looking for a way to do this without having to set the style to owner drawn and manually drawing the text of the nodes being added.
public class CustomTreeView : TreeView
{
// Fields for the custom checkbox images
private Image _checkedImage = Properties.Resources.CheckIcon;
private Image _uncheckedImage = Properties.Resources.UnCheckIcon;
private Image _intermediateImage = Properties.Resources.IntermediateCheckIcon;
private ImageList _imageList;
public CustomTreeView()
{
this.CheckBoxes = false;
_imageList = new ImageList();
_imageList.ImageSize = new Size(26, 26);
_imageList.Images.Add("Unchecked", _uncheckedImage);
_imageList.Images.Add("Checked", _checkedImage);
_imageList.Images.Add("Intermediate", _intermediateImage);
this.ImageList = _imageList;
this.Font = new Font("Gadugi", 12f);
this.ForeColor = Color.DodgerBlue;
this.BackColor = SystemColors.Control;
this.BorderStyle = BorderStyle.None;
this.Scrollable = true;
this.ShowLines = false;
this.ShowRootLines = false;
this.ShowNodeToolTips = true;
this.ShowPlusMinus = false;
this.HideSelection = false;
this.ItemHeight = 30;
}
private int GetEffectiveState(TreeNode node)
{
if (node.Nodes.Count == 0)
{
return node.Checked ? 1 : 0;
}
else
{
bool allChecked = true;
bool anyChecked = false;
foreach (TreeNode child in node.Nodes)
{
int childState = GetEffectiveState(child);
if (childState == 0)
allChecked = false;
else if (childState == 1)
anyChecked = true;
else
{
allChecked = false;
anyChecked = true;
}
}
if (allChecked)
return 1;
else if (anyChecked)
return 2;
else
return 0;
}
}
private void UpdateStateImageIndex(TreeNode node)
{
node.ImageIndex = GetEffectiveState(node);
node.SelectedImageIndex = node.ImageIndex;
}
private void SetNodeChecked(TreeNode node, bool checkedState)
{
node.Checked = checkedState;
foreach (TreeNode child in node.Nodes)
{
SetNodeChecked(child, checkedState);
}
}
private void UpdateStateIndices(TreeNode node)
{
while (node != null)
{
UpdateStateImageIndex(node);
node = node.Parent;
}
}
protected override void OnMouseDown(MouseEventArgs e)
{
TreeViewHitTestInfo hitTestInfo = HitTest(e.Location);
if (hitTestInfo.Location == TreeViewHitTestLocations.Image)
{
TreeNode node = hitTestInfo.Node;
if (node != null)
{
int currentState = GetEffectiveState(node);
if (currentState == 1)
{
SetNodeChecked(node, false);
}
else
{
SetNodeChecked(node, true);
}
UpdateStateIndices(node);
}
}
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
TreeViewHitTestInfo hitTestInfo = HitTest(e.Location);
if (hitTestInfo.Location == TreeViewHitTestLocations.Image)
{
this.Cursor = Cursors.Hand;
}
else
{
this.Cursor = Cursors.Default;
}
}
protected override void OnAfterCheck(TreeViewEventArgs e)
{
base.OnAfterCheck(e);
UpdateStateIndices(e.Node);
}
private void UpdateStateIndicesRecursive(TreeNode node)
{
node.Expand();
foreach (TreeNode child in node.Nodes)
{
UpdateStateIndicesRecursive(child);
}
UpdateStateImageIndex(node);
}
public void UpdateAllStateIndices()
{
foreach (TreeNode node in this.Nodes)
{
UpdateStateIndicesRecursive(node);
}
}
protected override void OnBeforeCollapse(TreeViewCancelEventArgs e)
{
e.Cancel = true;
}
protected override void OnKeyDown(KeyEventArgs e)
{
if (e.KeyCode == Keys.Up || e.KeyCode == Keys.Down || e.KeyCode == Keys.Left || e.KeyCode == Keys.Right)
{
e.Handled = true;
}
base.OnKeyDown(e);
}
protected override void OnBeforeSelect(TreeViewCancelEventArgs e)
{
e.Cancel = true;
base.OnBeforeSelect(e);
}
}
Share
Improve this question
asked Mar 22 at 1:40
VoidSharpVoidSharp
455 bronze badges
2
|
2 Answers
Reset to default 1The effect you see is caused by the attempt to solve an issue with the Win32 Control: when you show a ContextMenuStrip clicking on a TreeNode, the TreeView enters an undetermined state, in relation to that Node.
The .NET code forces the redraw of the Node, which would otherwise remain permanently selected in this scenario, sending a TVM_SELECTITEM message, specifying TVGN_DROPHILITE
(the state of a Node when a Drag&Drop operation is performed).
You can override WndProc and trap WM_LBUTTONDOWN
to change this behavior.
When the left Mouse button is down on an TreeNode, you test whether the pointer is over an Icon a State Icon or the Item itself (other location can be tested as well). In the first two cases, you send a TVM_SELECTITEM
message, specifying TVGN_CARET
, which sets the current selection (so the parent Control receives selection changing and selection changed events), but no highlight is performed because you have e.Cancel = true
in OnBeforeSelect
.
Otherwise, you call OnMouseDown()
with new MouseEventArgs
values.
The rest can remain as it is, I assume, because nothing else appears to interfere with this setup.
See whether this can solve the issue at hand.
As noted, depending on the use-case, the WM_LBUTTONDOWN
override may require a reimplementation of some of the safety features, which handle some edge cases, that are left out when the override is in place. This cannot be done directly because internal methods / properties are used in the source code
Here, I'm using the TVM_HITTEST message to test the Mouse location.
You can also use the Control's HitTest()
method.
public class CustomTreeView : TreeView {
// [...]
protected override void OnBeforeSelect(TreeViewCancelEventArgs e) {
e.Cancel = true;
base.OnBeforeSelect(e);
}
protected override void WndProc(ref Message m) {
switch (m.Msg) {
case WM_LBUTTONDOWN:
var pos = new Point((short)m.LParam.ToInt32(),
(m.LParam.ToInt32() >> 16) & 0xFFFF);
var hitInfo = new TVHITTESTINFO(pos);
SendMessage(Handle, TVM_HITTEST, 0, hitInfo);
if ((hitInfo.flags & TVHT_ONITEMICON) != 0 &&
(hitInfo.flags & TVHT_ONITEMSTATEICON) != 0) {
SendMessage(Handle, TVM_SELECTITEM, TVGN_CARET, IntPtr.Zero);
}
else {
OnMouseDown(new MouseEventArgs(MouseButtons.Left, 1, pos.X, pos.Y, 0));
}
m.Result = IntPtr.Zero;
return;
}
base.WndProc(ref m);
}
// Win32 declarations
const int WM_LBUTTONDOWN = 0x0201;
const int TVHT_ONITEMICON = 0x0002;
const int TVHT_ONITEMSTATEICON = 0x0040;
const int TVGN_CARET = 0x0009;
const uint TVM_HITTEST = 0x1111;
const uint TVM_SELECTITEM = 0x110B;
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public class TVHITTESTINFO {
public int pt_x;
public int pt_y;
public int flags = 0;
public IntPtr hItem = IntPtr.Zero;
public TVHITTESTINFO(Point p) { pt_x = p.X; pt_y = p.Y; }
}
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern int SendMessage(IntPtr hWnd, uint uMsg, int wParam, TVHITTESTINFO lParam);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern int SendMessage(IntPtr hWnd, uint uMsg, int wParam, IntPtr lParam);
// [...]
}
After some insight from Jimi in the comments of the post. I have come to a solution using WndProc
.
Below was my solution to resolve this issue but as a side note, as mentioned by Jimi
this requires strict testing, and, in the end, also might require (depending on the use-case) re-implementing some of the safety features (that handle some edge cases) that are left out when the override is in place
private const int WM_LBUTTONDOWN = 0x0201; // Left mouse button down
protected override void WndProc(ref Message m)
{
//Checks if the Left mouse button down is being sent as the message
if (m.Msg == WM_LBUTTONDOWN)
{
//Gathers click location for HitTest
int x = (int)(m.LParam.ToInt64() & 0xFFFF);
int y = (int)((m.LParam.ToInt64() >> 16) & 0xFFFF);
Point clickPoint = new Point(x, y);
TreeViewHitTestInfo hitTestInfo = HitTest(clickPoint);
if (hitTestInfo.Node != null)
{
//For my case, I am using a Image as a checkbox icon, this section handles the selection of this.
if (hitTestInfo.Location == TreeViewHitTestLocations.Image)
{
int currentState = GetEffectiveState(hitTestInfo.Node);
// Toggle check state
SetNodeChecked(hitTestInfo.Node, currentState != 1);
UpdateStateIndices(hitTestInfo.Node);
// Suppress selection
m.Msg = 0; // Block the message
return;
} //This section is used for handling the click of the label, or just right of the label (preventing highlight)
else if (hitTestInfo.Location == TreeViewHitTestLocations.Label ||
hitTestInfo.Location == TreeViewHitTestLocations.RightOfLabel)
{
return; // Do nothing, preventing selection highlight
}
}
}
base.WndProc(ref m);
}
I did my best to provide context of what is happening on each line. Thanks again Jimi
发布者:admin,转转请注明出处:http://www.yc00.com/questions/1744327437a4568719.html
TreeViewHitTestLocations.Image
, it prevents it but the functionality of the checkbox is lost (requires 2 clicks or more to get a toggle) Sidenote: This is not a commercial app in the sense of being sold, however it will be distributed among my team at work. – VoidSharp Commented Mar 22 at 19:43