Background:
I have a Forms.ComboBox with a DropDownStyle = DropDown.
I don't use AutoComplete, but I implemented something similar which does not only filter the beginning of the text, but uses a regular expression and shows all items which match the text entered. This works fine.
However, when I type the first letter of a matching item, the ComboBox falls back to its original behavior and sets DroppedDown = true and auto selects the first entry and completes the text to match the selected item (similar to AutoCompleteMode Append). What I want is no auto selection and auto completion.
What I found so far is, that I somehow have to prevent SendMessage() with CB_FINDSTRING of being called and replace CB_FINDSTRING with CB_FINDSTRINGEXACT (MSDN Link).
I think I have to extend the ComboBox class, but I'm not sure which methods I have to override. I'm working with C# .NET Framework v3.5.
Questions:
How do I extend a Windows.Forms.ComboBox to prevent the auto select behavior?
Links:
How can I prevent auto-select in ComboBox on drop-down except for exact matches? (did not help me)
Try this:
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using Opulos.Core.Win32;
namespace Opulos.Core.UI {
// Extension class to disable the auto-select behavior when a combobox is in DropDown mode.
public static class ComboBoxAutoSelectEx {
public static void AutoSelectOff(this ComboBox combo) {
Data.Register(combo);
}
public static void AutoSelectOn(this ComboBox combo) {
Data data = null;
if (Data.dict.TryGetValue(combo, out data)) {
data.Dispose();
Data.dict.Remove(combo);
}
}
private class Data {
// keep a reference to the native windows so they don't get disposed
internal static Dictionary<ComboBox, Data> dict = new Dictionary<ComboBox, Data>();
// a ComboBox consists of 3 windows (combobox handle, text edit handle and dropdown list handle)
ComboBox combo;
NW nwList = null; // handle to the combobox's dropdown list
NW2 nwEdit = null; // handle to the edit window
internal void Dispose() {
dict.Remove(this.combo);
this.nwList.ReleaseHandle();
this.nwEdit.ReleaseHandle();
}
public static void Register(ComboBox combo) {
if (dict.ContainsKey(combo))
return; // already registered
Data data = new Data() { combo = combo };
Action assign = () => {
if (dict.ContainsKey(combo))
return; // already assigned
COMBOBOXINFO info = COMBOBOXINFO.GetInfo(combo); // new COMBOBOXINFO();
//info.cbSize = Marshal.SizeOf(info);
//COMBOBOXINFO2.SendMessageCb(combo.Handle, 0x164, IntPtr.Zero, out info);
dict[combo] = data;
data.nwList = new NW(combo, info.hwndList);
data.nwEdit = new NW2(info.hwndEdit);
};
if (!combo.IsHandleCreated)
combo.HandleCreated += delegate { assign(); };
else
assign();
combo.HandleDestroyed += delegate {
data.Dispose();
};
}
}
private class NW : NativeWindow {
ComboBox combo;
public NW(ComboBox combo, IntPtr handle) {
this.combo = combo;
AssignHandle(handle);
}
private const int LB_FINDSTRING = 0x018F;
private const int LB_FINDSTRINGEXACT = 0x01A2;
protected override void WndProc(ref Message m) {
if (m.Msg == LB_FINDSTRING) {
m.Msg = LB_FINDSTRINGEXACT;
}
base.WndProc(ref m);
if (m.Msg == LB_FINDSTRINGEXACT) {
String find = Marshal.PtrToStringAuto(m.LParam);
for (int i = 0; i < combo.Items.Count; i++) {
Object item = combo.Items[i];
if (item.Equals(find)) {
m.Result = new IntPtr(i);
break;
}
}
}
}
}
private class NW2 : NativeWindow {
public NW2(IntPtr handle) {
AssignHandle(handle);
}
private const int EM_SETSEL = 0x00B1;
private const int EM_GETSEL = 0x00B0;
protected override void WndProc(ref Message m) {
if (m.Msg == EM_SETSEL) {
// if this code is not here, then the entire combobox text is selected
// which looks ugly, especially when there are multiple combo boxes.
//
// if this method returns immediately, then the caret position is set
// to (0, 0). However, it seems that calling EM_GETSEL has a side effect
// that the caret position is mostly maintained. Sometimes it slips back
// to (0, 0).
SendMessage(Handle, EM_GETSEL, IntPtr.Zero, IntPtr.Zero);
//int selStart = (sel & 0x00ff);
//int selEnd = (sel >> 16) & 0x00ff;
//Debug.WriteLine("EM_GETSEL: " + selStart + " nEnd: " + selEnd);
return;
}
base.WndProc(ref m);
}
[DllImportAttribute("user32.dll", SetLastError=true)]
private static extern int SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
}
}
[StructLayout(LayoutKind.Sequential)]
public struct COMBOBOXINFO {
public Int32 cbSize;
public RECT rcItem;
public RECT rcButton;
public int buttonState;
public IntPtr hwndCombo;
public IntPtr hwndEdit;
public IntPtr hwndList;
public static COMBOBOXINFO GetInfo(ComboBox combo) {
COMBOBOXINFO info = new COMBOBOXINFO();
info.cbSize = Marshal.SizeOf(info);
SendMessageCb(combo.Handle, 0x164, IntPtr.Zero, out info);
return info;
}
[DllImport("user32.dll", EntryPoint = "SendMessageW", CharSet = CharSet.Unicode)]
private static extern IntPtr SendMessageCb(IntPtr hWnd, int msg, IntPtr wp, out COMBOBOXINFO lp);
}
//[StructLayout(LayoutKind.Sequential)]
//public struct RECT {
// public int Left;
// public int Top;
// public int Right;
// public int Bottom;
//}
}
Related
I used:
myComboBox.DrawMode = DrawMode.OwnerDrawFixed;
Then I implemented the DrawItem event like so:
private void myComboBox_DrawItem(object sender, DrawItemEventArgs e)
{
if (e.Index < 0)
return;
string text = myComboBox.GetItemText(myComboBox.Items[e.Index]);
e.DrawBackground();
using var brush = new SolidBrush(e.ForeColor);
e.Graphics.DrawString(text, e.Font!, brush, e.Bounds);
if (myComboBox.DroppedDown && (e.State & DrawItemState.Selected) == DrawItemState.Selected)
{
var item = (IndexedItem)myComboBox.Items[e.Index]; // IndexedItem is my own class to hold the index and name of a combobox item
if (!string.IsNullOrWhiteSpace(item.Name))
myToolTip.Show(item.Name, myComboBox, e.Bounds.Right, e.Bounds.Bottom);
}
e.DrawFocusRectangle();
}
The purpose is to show a tooltip for each item when I hover it. The above code works fine but sometimes the dropdown is shown above instead of below due to available space.
The problem now is, that DrawBackground etc use the adjusted positions but e.Bounds still contains the area below the control. So the dropdown is shown above but the tooltip below the combobox. I guess DrawBackground modifies the bounds internally but doesn't write the adjusted value back to e.Bounds. Now the question is, how can I determine the correct bounds of the item in that case?
Ok I managed to find the correct bounds via interops like this:
Rectangle GetItemBounds(int index)
{
const int SB_VERT = 0x1;
const int SIF_RANGE = 0x1;
const int SIF_POS = 0x4;
const uint GETCOMBOBOXINFO = 0x0164;
var info = new COMBOBOXINFO();
info.cbSize = Marshal.SizeOf(info);
SendMessageW(
myComboBox.Handle,
GETCOMBOBOXINFO,
IntPtr.Zero,
ref info);
GetWindowRect(info.hwndList, out RECT rc);
var dropdownArea = new Rectangle(myComboBox.PointToClient(rc.Location), rc.Size);
int yDiff = index * myComboBox.ItemHeight;
var scrollInfo = new SCROLLINFO();
scrollInfo.cbSize = (uint)Marshal.SizeOf(scrollInfo);
scrollInfo.fMask = SIF_RANGE | SIF_POS;
GetScrollInfo(info.hwndList, SB_VERT, ref scrollInfo);
int scrollY = scrollInfo.nPos * myComboBox.ItemHeight;
return new Rectangle(dropdownArea.X, dropdownArea.Y - scrollY + yDiff, rc.Width, myComboBox.ItemHeight);
}
And then use it like this:
var realBounds = GetItemBounds(e.Index);
I am not sure if this is the best approach but it even works with scrolling inside the dropdown now.
The interops look like this:
[StructLayout(LayoutKind.Sequential)]
struct COMBOBOXINFO
{
public Int32 cbSize;
public RECT rcItem, rcButton;
public int buttonState;
public IntPtr hwndCombo, hwndEdit, hwndList;
}
[Serializable, StructLayout(LayoutKind.Sequential)]
struct SCROLLINFO
{
public uint cbSize;
public uint fMask;
public int nMin;
public int nMax;
public uint nPage;
public int nPos;
public int nTrackPos;
}
[DllImport("user32")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetScrollInfo(IntPtr hwnd, int fnBar, ref SCROLLINFO lpsi);
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int Left, Top, Right, Bottom;
public RECT(int left, int top, int right, int bottom)
{
Left = left;
Top = top;
Right = right;
Bottom = bottom;
}
public RECT(System.Drawing.Rectangle r) : this(r.Left, r.Top, r.Right, r.Bottom) { }
public int X
{
get { return Left; }
set { Right -= (Left - value); Left = value; }
}
public int Y
{
get { return Top; }
set { Bottom -= (Top - value); Top = value; }
}
public int Height
{
get { return Bottom - Top; }
set { Bottom = value + Top; }
}
public int Width
{
get { return Right - Left; }
set { Right = value + Left; }
}
public System.Drawing.Point Location
{
get { return new System.Drawing.Point(Left, Top); }
set { X = value.X; Y = value.Y; }
}
public System.Drawing.Size Size
{
get { return new System.Drawing.Size(Width, Height); }
set { Width = value.Width; Height = value.Height; }
}
public static implicit operator System.Drawing.Rectangle(RECT r)
{
return new System.Drawing.Rectangle(r.Left, r.Top, r.Width, r.Height);
}
public static implicit operator RECT(System.Drawing.Rectangle r)
{
return new RECT(r);
}
public static bool operator ==(RECT r1, RECT r2)
{
return r1.Equals(r2);
}
public static bool operator !=(RECT r1, RECT r2)
{
return !r1.Equals(r2);
}
public bool Equals(RECT r)
{
return r.Left == Left && r.Top == Top && r.Right == Right && r.Bottom == Bottom;
}
public override bool Equals(object? obj)
{
if (obj is RECT)
return Equals((RECT)obj);
else if (obj is System.Drawing.Rectangle)
return Equals(new RECT((System.Drawing.Rectangle)obj));
return false;
}
public override int GetHashCode()
{
return ((System.Drawing.Rectangle)this).GetHashCode();
}
public override string ToString()
{
return string.Format(System.Globalization.CultureInfo.CurrentCulture, "{{Left={0},Top={1},Right={2},Bottom={3}}}", Left, Top, Right, Bottom);
}
}
[DllImport("user32")]
private static extern bool GetWindowRect(IntPtr hWnd, out RECT rc);
[DllImport("user32", ExactSpelling = true)]
public static extern IntPtr SendMessageW(
IntPtr hWnd,
uint Msg,
IntPtr wParam = default,
IntPtr lParam = default);
public unsafe static IntPtr SendMessageW<T>(
IntPtr hWnd,
uint Msg,
IntPtr wParam,
ref T lParam) where T : unmanaged
{
fixed (void* l = &lParam)
{
return SendMessageW(hWnd, Msg, wParam, (IntPtr)l);
}
}
I created an extension:
[EditorBrowsable(EditorBrowsableState.Advanced)]
public static class ComboBoxExtensions
{
public static Rectangle GetItemBounds(this ComboBox comboBox, int index)
{
const int SB_VERT = 0x1;
const int SIF_RANGE = 0x1;
const int SIF_POS = 0x4;
const uint GETCOMBOBOXINFO = 0x0164;
var info = new Interop.COMBOBOXINFO();
info.cbSize = Marshal.SizeOf(info);
Interop.SendMessageW(
comboBox.Handle,
GETCOMBOBOXINFO,
IntPtr.Zero,
ref info);
Interop.GetWindowRect(info.hwndList, out Interop.RECT rc);
var dropdownArea = new Rectangle(comboBox.PointToClient(rc.Location), rc.Size);
int yDiff = index * comboBox.ItemHeight;
var scrollInfo = new Interop.SCROLLINFO();
scrollInfo.cbSize = (uint)Marshal.SizeOf(scrollInfo);
scrollInfo.fMask = SIF_RANGE | SIF_POS;
Interop.GetScrollInfo(info.hwndList, SB_VERT, ref scrollInfo);
int scrollY = scrollInfo.nPos * comboBox.ItemHeight;
return new Rectangle(dropdownArea.X, dropdownArea.Y - scrollY + yDiff, rc.Width, comboBox.ItemHeight);
}
}
I realize you have already answered your own question. This is just plan B.
Your post mentions that the purpose is to show a tooltip for each item when I hover it so this answer focuses on that aspect. It doesn't make use of the item bounds info but that would be available in the DrawItem handler if you still need it.
This approach is loosely based on this answer and takes advantage of comboBox.SelectedItem property changing (but without firing an event) as the mouse moves over the dropped-down list. It uses that change to draw tool tips on an owner-draw combo box (only it doesn't have to be an owner-draw combo box for it to work).
This sample code for MainForm starts a timer when the ComboBox drops and ends it when it closes. It polls the SelectedIndex property on each tick.
public MainForm()
{
InitializeComponent();
// Works with or without owner draw.
// This line may be commented out
comboBox.DrawMode = DrawMode.OwnerDrawFixed;
// Add items
foreach (var item in Enum.GetValues(typeof(ComboBoxItems)))
comboBox.Items.Add(item);
// Start timer on drop down
comboBox.DropDown += (sender, e) => _toolTipTimer.Enabled = true;
// End timer on closed
comboBox.DropDownClosed += (sender, e) =>
{
_toolTipTimer.Enabled = false;
_lastToolTipIndex = -1;
_toolTip.Hide(comboBox);
};
// Poll index while open
_toolTipTimer.Tick += (sender, e) =>
{
if(comboBox.SelectedIndex != _lastToolTipIndex)
{
_lastToolTipIndex = comboBox.SelectedIndex;
showToolTip();
}
};
If there is a new index, the local method showToolTip is invoked to show the correct text.
void showToolTip()
{
if (_lastToolTipIndex != -1)
{
// Get the item
var item = (ComboBoxItems)comboBox.Items[_lastToolTipIndex];
// Get the tip
var tt = TipAttribute.FromMember(item);
// Get the rel pos
var mousePosition = PointToClient(MousePosition);
var rel = new Point(
(mousePosition.X - comboBox.Location.X) - 10,
(mousePosition.Y - comboBox.Location.Y) - 30);
// Show the tip
_toolTip.Show(tt, comboBox, rel);
}
}
}
private ToolTip _toolTip = new ToolTip();
private readonly Timer _toolTipTimer = new Timer() { Interval = 100 };
private int _lastToolTipIndex = -1;
The list items are represented by this enum:
enum ComboBoxItems
{
[Tip("...a day keeps the doctor away")]
Apple,
[Tip("...you glad I didn't say 'banana?'")]
Orange,
[Tip("...job on the Tool Tips!")]
Grape,
}
Where TipAttribute is defined as:
internal class TipAttribute : Attribute
{
public TipAttribute(string tip) => Tip = tip;
public string Tip { get; }
public static string FromMember(Enum value) =>
((TipAttribute)value
.GetType()
.GetMember($"{value}")
.Single()
.GetCustomAttribute(typeof(TipAttribute))).Tip;
}
I am interested in removing the text selection of ComboBoxes with DropDownStyle = DropDown.
When I add/remove or close the DropPown, then the Item is selected.
I am not able to clear the selected text. Do you have some idea how to do this?
This code does not work:
comboBox.SelectionLenght = 0;
comboBox.SelectionStart = comboBox.Text.Legnth;
comboBox.Select(0,0);
I can see that the text is highlighted after this line:
selectedComboBox.Items.Add(redCompetitorName);
You can defer the execution of the Select() method, calling BeginInvoke() in the SelectedIndexChanged event handler (or SelectionChangedCommitted, if you want this to happen only when a User selects an Item manually).
By deferring the execution (this action is enqueued in the message loop), the Select() action is performed only after the ComboBox.Text has been set and highlighted. So your command is not overridden by the default behavior.
private void comboBox_SelectedIndexChanged(object sender, EventArgs e)
{
// Set the caret to the start of the ComboBox text
BeginInvoke(new Action(()=> comboBox.Select(0, 0)));
// OR - set the caret to the end of the text instead
BeginInvoke(new Action(()=> comboBox.Select(int.MaxValue, 0)));
}
This concept applies in other contexts of course.
You can use this method in other situations, when you need to perform an action, in the UI, that is executed only after the current (or the current sequence of the already scheduled actions) has completed.
If you want a more involved solution that prevents all kind of selection highlights in a ComboBox, you can use a Custom Control derived from ComboBox, get the Handle of its Edit Control, use a NativeWindow to intercept its messages. Override WndProc to handle EM_SETSEL and call PostMessage to remove the selection (only when the starting position is > 0, otherwise you risk to get stuck in a weird auto-loop which has an effect that's usually referred to as StackOverflow :).
using System.ComponentModel;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;
[DesignerCategory("code")]
public class ComboBoxNoFocus : ComboBox
{
IntPtr editHandle = IntPtr.Zero;
private EditNativeWindow editControl = null;
public ComboBoxNoFocus() { }
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
editHandle = GetComboBoxEditInternal(this.Handle);
editControl = new EditNativeWindow(editHandle);
}
public class EditNativeWindow : NativeWindow
{
private const int EM_SETSEL = 0x0B1;
public EditNativeWindow() : this(IntPtr.Zero) { }
public EditNativeWindow(IntPtr handle)
{
if (handle != IntPtr.Zero) {
this.AssignHandle(handle);
}
}
protected override void WndProc(ref Message m)
{
switch (m.Msg) {
case EM_SETSEL:
int pos = m.LParam.ToInt32();
if (pos > 0) {
PostMessage(this.Handle, EM_SETSEL, 0, 0);
}
return;
default:
// Other operations
break;
}
base.WndProc(ref m);
}
}
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
internal static extern bool GetComboBoxInfo(IntPtr hWnd, ref COMBOBOXINFO pcbi);
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern bool PostMessage(IntPtr hWnd, uint Msg, int wParam, int lParam);
[StructLayout(LayoutKind.Sequential)]
internal struct COMBOBOXINFO
{
public int cbSize;
public Rectangle rcItem;
public Rectangle rcButton;
public int buttonState;
public IntPtr hwndCombo;
public IntPtr hwndEdit;
public IntPtr hwndList;
public void Init() => this.cbSize = Marshal.SizeOf<COMBOBOXINFO>();
}
internal static IntPtr GetComboBoxEditInternal(IntPtr cboHandle)
{
var cbInfo = new COMBOBOXINFO();
cbInfo.Init();
GetComboBoxInfo(cboHandle, ref cbInfo);
return cbInfo.hwndEdit;
}
}
Set the focus to another control in your form after a new item is selected, using the SelectedIndexChanged event of the Combobox.
private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
{
button1.Focus();
}
So I have the following code:
#region Dropshadow
[DllImport("Gdi32.dll", EntryPoint = "CreateRoundRectRgn")]
private static extern IntPtr CreateRoundRectRgn
(
int nLeftRect,
int nTopRect,
int nRightRect,
int nBottomRect,
int nWidthEllipse,
int nHeightEllipse
);
[DllImport("dwmapi.dll")]
public static extern int DwmExtendFrameIntoClientArea(IntPtr hWnd, ref MARGINS pMarInset);
[DllImport("dwmapi.dll")]
public static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize);
[DllImport("dwmapi.dll")]
public static extern int DwmIsCompositionEnabled(ref int pfEnabled);
private bool m_aeroEnabled;
public struct MARGINS
{
public int leftWidth;
public int rightWidth;
public int topHeight;
public int bottomHeight;
}
protected override CreateParams CreateParams {
get {
m_aeroEnabled = CheckAeroEnabled();
CreateParams cp = base.CreateParams;
if (!m_aeroEnabled) {
cp.ClassStyle |= 0x00020000;
}
return cp;
}
}
private bool CheckAeroEnabled()
{
if (Environment.OSVersion.Version.Major >= 6) {
int enabled = 0;
DwmIsCompositionEnabled(ref enabled);
return (enabled == 1) ? true : false;
}
return false;
}
protected override void WndProc(ref Message m)
{
switch (m.Msg) {
case 0x0085:
if (m_aeroEnabled) {
int v = 2;
DwmSetWindowAttribute(Handle, 2, ref v, 4);
MARGINS margins = new MARGINS() {
bottomHeight = 1,
leftWidth = 0,
rightWidth = 0,
topHeight = 0
};
DwmExtendFrameIntoClientArea(Handle, ref margins);
}
break;
default:
break;
}
base.WndProc(ref m);
}
#endregion
This makes a Dropshadow using GDI.
The only issue however, is I had to make it keep a 1 pixel height border on the top (it can be any edge, just top is hardest to notice on my app).
This makes a line on my app at the top essentially degrading viewing experience.
Is it possible to do this with no border at all?
(The bottomHeight = 1 code is where its all about. If I set it to 0, and topHeight to 1, the line will be on the bottom. Setting all of them to 0, shows no dropshadow at all.)
Turns out, its to do with my padding, I need to leave 1 pixel line empty on atleast 1 edge for the Dropshadow to work. I chose to use Padding to make that 1 pixel line and I set the top padding to 1. This sets the line at the top. The bottomHeight = 1 doesnt matter at all. It's just there as it requires atleast one of them to be non 0.
If I remove the Padding and Top Line etc. And in the CreateParams overide, if I remove the aero enabled check, it shows a dropshadow similar like this:
This is a Form class that uses DWM to render it's borders/shadow.
As described, you need to register an Attribute, DWMWINDOWATTRIBUTE, and the related Policy, DWMNCRENDERINGPOLICY, settings it's value to Enabled.
Then set the attribute with DwmSetWindowAttribute() and the desired effect with DwmExtendFrameIntoClientArea(), DwmEnableBlurBehindWindow() and so on.
All the declarations needed are here.
This is the Form class (named "Borderless", in a spark of creativity).
I tried to make it look like what you already have posted, to minimize the "impact".
The Form is a standard WinForms Form with FormBorderStyle = None.
public partial class Borderless : Form
{
public Borderless() => InitializeComponent();
protected override void OnHandleCreated(EventArgs e) {
base.OnHandleCreated(e);
WinApi.Dwm.DWMNCRENDERINGPOLICY Policy = WinApi.Dwm.DWMNCRENDERINGPOLICY.Enabled;
WinApi.Dwm.WindowSetAttribute(this.Handle, WinApi.Dwm.DWMWINDOWATTRIBUTE.NCRenderingPolicy, (int)Policy);
if (DWNCompositionEnabled()) { WinApi.Dwm.WindowBorderlessDropShadow(this.Handle, 2); }
//if (DWNCompositionEnabled()) { WinApi.Dwm.WindowEnableBlurBehind(this.Handle); }
//if (DWNCompositionEnabled()) { WinApi.Dwm.WindowSheetOfGlass(this.Handle); }
}
private bool DWNCompositionEnabled() => (Environment.OSVersion.Version.Major >= 6)
? WinApi.Dwm.IsCompositionEnabled()
: false;
protected override void WndProc(ref Message m)
{
switch (m.Msg)
{
case (int)WinApi.WinMessage.WM_DWMCOMPOSITIONCHANGED:
{
WinApi.Dwm.DWMNCRENDERINGPOLICY Policy = WinApi.Dwm.DWMNCRENDERINGPOLICY.Enabled;
WinApi.Dwm.WindowSetAttribute(this.Handle, WinApi.Dwm.DWMWINDOWATTRIBUTE.NCRenderingPolicy, (int)Policy);
WinApi.Dwm.WindowBorderlessDropShadow(this.Handle, 2);
m.Result = (IntPtr)0;
}
break;
default:
break;
}
base.WndProc(ref m);
}
}
These are all the declarations needed, plus others that might become useful.
Note that I only use the internal attribute form Win32 APIs, which are called using helpr methods.
It's a partial class because the Winapi class is a extensive class library. You can change it to whatever you are used to.
I suggest to keep the [SuppressUnmanagedCodeSecurityAttribute]
attribute for the Win32 APIs declarations.
public partial class WinApi
{
public enum WinMessage : int
{
WM_DWMCOMPOSITIONCHANGED = 0x031E, //The system will send a window the WM_DWMCOMPOSITIONCHANGED message to indicate that the availability of desktop composition has changed.
WM_DWMNCRENDERINGCHANGED = 0x031F, //WM_DWMNCRENDERINGCHANGED is called when the non-client area rendering status of a window has changed. Only windows that have set the flag DWM_BLURBEHIND.fTransitionOnMaximized to true will get this message.
WM_DWMCOLORIZATIONCOLORCHANGED = 0x0320, //Sent to all top-level windows when the colorization color has changed.
WM_DWMWINDOWMAXIMIZEDCHANGE = 0x0321 //WM_DWMWINDOWMAXIMIZEDCHANGE will let you know when a DWM composed window is maximized. You also have to register for this message as well. You'd have other windowd go opaque when this message is sent.
}
public class Dwm
public enum DWMWINDOWATTRIBUTE : uint
{
NCRenderingEnabled = 1, //Get only atttribute
NCRenderingPolicy, //Enable or disable non-client rendering
TransitionsForceDisabled,
AllowNCPaint,
CaptionButtonBounds,
NonClientRtlLayout,
ForceIconicRepresentation,
Flip3DPolicy,
ExtendedFrameBounds,
HasIconicBitmap,
DisallowPeek,
ExcludedFromPeek,
Cloak,
Cloaked,
FreezeRepresentation
}
public enum DWMNCRENDERINGPOLICY : uint
{
UseWindowStyle, // Enable/disable non-client rendering based on window style
Disabled, // Disabled non-client rendering; window style is ignored
Enabled, // Enabled non-client rendering; window style is ignored
};
// Values designating how Flip3D treats a given window.
enum DWMFLIP3DWINDOWPOLICY : uint
{
Default, // Hide or include the window in Flip3D based on window style and visibility.
ExcludeBelow, // Display the window under Flip3D and disabled.
ExcludeAbove, // Display the window above Flip3D and enabled.
};
public struct MARGINS
{
public int leftWidth;
public int rightWidth;
public int topHeight;
public int bottomHeight;
public MARGINS(int LeftWidth, int RightWidth, int TopHeight, int BottomHeight)
{
leftWidth = LeftWidth;
rightWidth = RightWidth;
topHeight = TopHeight;
bottomHeight = BottomHeight;
}
public void NoMargins()
{
leftWidth = 0;
rightWidth = 0;
topHeight = 0;
bottomHeight = 0;
}
public void SheetOfGlass()
{
leftWidth = -1;
rightWidth = -1;
topHeight = -1;
bottomHeight = -1;
}
}
[SuppressUnmanagedCodeSecurityAttribute]
internal static class SafeNativeMethods
{
//https://msdn.microsoft.com/en-us/library/windows/desktop/aa969508(v=vs.85).aspx
[DllImport("dwmapi.dll")]
internal static extern int DwmEnableBlurBehindWindow(IntPtr hwnd, ref DWM_BLURBEHIND blurBehind);
//https://msdn.microsoft.com/it-it/library/windows/desktop/aa969512(v=vs.85).aspx
[DllImport("dwmapi.dll")]
internal static extern int DwmExtendFrameIntoClientArea(IntPtr hWnd, ref MARGINS pMarInset);
//https://msdn.microsoft.com/en-us/library/windows/desktop/aa969515(v=vs.85).aspx
[DllImport("dwmapi.dll")]
internal static extern int DwmGetWindowAttribute(IntPtr hwnd, DWMWINDOWATTRIBUTE attr, ref int attrValue, int attrSize);
//https://msdn.microsoft.com/en-us/library/windows/desktop/aa969524(v=vs.85).aspx
[DllImport("dwmapi.dll")]
internal static extern int DwmSetWindowAttribute(IntPtr hwnd, DWMWINDOWATTRIBUTE attr, ref int attrValue, int attrSize);
[DllImport("dwmapi.dll")]
internal static extern int DwmIsCompositionEnabled(ref int pfEnabled);
}
public static bool IsCompositionEnabled()
{
int pfEnabled = 0;
int result = SafeNativeMethods.DwmIsCompositionEnabled(ref pfEnabled);
return (pfEnabled == 1) ? true : false;
}
public static bool IsNonClientRenderingEnabled(IntPtr hWnd)
{
int gwaEnabled = 0;
int result = SafeNativeMethods.DwmGetWindowAttribute(hWnd, DWMWINDOWATTRIBUTE.NCRenderingEnabled, ref gwaEnabled, sizeof(int));
return (gwaEnabled == 1) ? true : false;
}
public static bool WindowSetAttribute(IntPtr hWnd, DWMWINDOWATTRIBUTE Attribute, int AttributeValue)
{
int result = SafeNativeMethods.DwmSetWindowAttribute(hWnd, Attribute, ref AttributeValue, sizeof(int));
return (result == 0);
}
public static bool WindowEnableBlurBehind(IntPtr hWnd)
{
//Create and populate the Blur Behind structure
DWM_BLURBEHIND Dwm_BB = new DWM_BLURBEHIND(true);
int result = SafeNativeMethods.DwmEnableBlurBehindWindow(hWnd, ref Dwm_BB);
return (result == 0);
}
public static bool WindowExtendIntoClientArea(IntPtr hWnd, WinApi.Dwm.MARGINS Margins)
{
// Extend frame on the bottom of client area
int result = SafeNativeMethods.DwmExtendFrameIntoClientArea(hWnd, ref Margins);
return (result == 0);
}
public static bool WindowBorderlessDropShadow(IntPtr hWnd, int ShadowSize)
{
MARGINS Margins = new MARGINS(0, ShadowSize, 0, ShadowSize);
int result = SafeNativeMethods.DwmExtendFrameIntoClientArea(hWnd, ref Margins);
return (result == 0);
}
public static bool WindowSheetOfGlass(IntPtr hWnd)
{
MARGINS Margins = new MARGINS();
Margins.SheetOfGlass();
//Margins set to All:-1 - Sheet Of Glass effect
int result = SafeNativeMethods.DwmExtendFrameIntoClientArea(hWnd, ref Margins);
return (result == 0);
}
public static bool WindowDisableRendering(IntPtr hWnd)
{
DWMNCRENDERINGPOLICY NCRP = DWMNCRENDERINGPOLICY.Disabled;
int ncrp = (int)NCRP;
// Disable non-client area rendering on the window.
int result = SafeNativeMethods.DwmSetWindowAttribute(hWnd, DWMWINDOWATTRIBUTE.NCRenderingPolicy, ref ncrp, sizeof(int));
return (result == 0);
}
}
}
I set the bottomHeight to 3, and I found that the border height was included in the form size (the form's size didn't change).
So I set a BackgroundImage to this form, and the border was hidden by the image.
I am using Visual Studio 2010 (C#) and a Windows Forms application.
I have two treeviews side by side, and I have figured out how to synchronise the scrolling using the up/down buttons on the scrollbar, but when I use the slider it does not move the other treeview. I have taken a listview example that works, but the same code does not work for treeviews.
So far I have, in the main form:
[DllImport("User32.dll")]
public static extern int SendMessage(IntPtr hWnd, uint Msg, uint wParam, uint lParam);
private void myListBox1_Scroll(ref Message m)
{
SendMessage(myListBox2.Handle, (uint)m.Msg, (uint)m.WParam, (uint)m.LParam);
}
I have created a control:
public partial class MyTreeView : TreeView
{
public MyTreeView()
{
InitializeComponent();
}
public event ScrollEventHandler Scroll;
public delegate void ScrollEventHandler(ref Message m);
private const int WM_VSCROLL = 0x115;
protected override void WndProc(ref System.Windows.Forms.Message m)
{
if (m.Msg == WM_VSCROLL)
if (Scroll != null)
{
Scroll(ref m);
}
base.WndProc(ref m);
}
}
which I add two of to the form.
I can use the same code to have a listivew control the treeview and that will work if you drag the slider, but in reverse it only works with the up down buttons.
Rather than use SendMessage and mark your DLL as unsafe you can use the GetScrollPos and SetScrollPos functions from user32.dll.
I've wrapped the code up into your MyTreeView class so it's nicely encapsulated.
You just need to call the AddLinkedTreeView method like so:
treeView1.AddLinkedTreeView(treeView2);
Here's the source for the MyTreeView class.
public partial class MyTreeView : TreeView
{
public MyTreeView() : base()
{
}
private List<MyTreeView> linkedTreeViews = new List<MyTreeView>();
/// <summary>
/// Links the specified tree view to this tree view. Whenever either treeview
/// scrolls, the other will scroll too.
/// </summary>
/// <param name="treeView">The TreeView to link.</param>
public void AddLinkedTreeView(MyTreeView treeView)
{
if (treeView == this)
throw new ArgumentException("Cannot link a TreeView to itself!", "treeView");
if (!linkedTreeViews.Contains(treeView))
{
//add the treeview to our list of linked treeviews
linkedTreeViews.Add(treeView);
//add this to the treeview's list of linked treeviews
treeView.AddLinkedTreeView(this);
//make sure the TreeView is linked to all of the other TreeViews that this TreeView is linked to
for (int i = 0; i < linkedTreeViews.Count; i++)
{
//get the linked treeview
var linkedTreeView = linkedTreeViews[i];
//link the treeviews together
if (linkedTreeView != treeView)
linkedTreeView.AddLinkedTreeView(treeView);
}
}
}
/// <summary>
/// Sets the destination's scroll positions to that of the source.
/// </summary>
/// <param name="source">The source of the scroll positions.</param>
/// <param name="dest">The destinations to set the scroll positions for.</param>
private void SetScrollPositions(MyTreeView source, MyTreeView dest)
{
//get the scroll positions of the source
int horizontal = User32.GetScrollPos(source.Handle, Orientation.Horizontal);
int vertical = User32.GetScrollPos(source.Handle, Orientation.Vertical);
//set the scroll positions of the destination
User32.SetScrollPos(dest.Handle, Orientation.Horizontal, horizontal, true);
User32.SetScrollPos(dest.Handle, Orientation.Vertical, vertical, true);
}
protected override void WndProc(ref Message m)
{
//process the message
base.WndProc(ref m);
//pass scroll messages onto any linked views
if (m.Msg == User32.WM_VSCROLL || m.Msg == User32.WM_MOUSEWHEEL)
{
foreach (var linkedTreeView in linkedTreeViews)
{
//set the scroll positions of the linked tree view
SetScrollPositions(this, linkedTreeView);
//copy the windows message
Message copy = new Message
{
HWnd = linkedTreeView.Handle,
LParam = m.LParam,
Msg = m.Msg,
Result = m.Result,
WParam = m.WParam
};
//pass the message onto the linked tree view
linkedTreeView.RecieveWndProc(ref copy);
}
}
}
/// <summary>
/// Recieves a WndProc message without passing it onto any linked treeviews. This is useful to avoid infinite loops.
/// </summary>
/// <param name="m">The windows message.</param>
private void RecieveWndProc(ref Message m)
{
base.WndProc(ref m);
}
/// <summary>
/// Imported functions from the User32.dll
/// </summary>
private class User32
{
public const int WM_VSCROLL = 0x115;
public const int WM_MOUSEWHEEL = 0x020A;
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern int GetScrollPos(IntPtr hWnd, System.Windows.Forms.Orientation nBar);
[DllImport("user32.dll")]
public static extern int SetScrollPos(IntPtr hWnd, System.Windows.Forms.Orientation nBar, int nPos, bool bRedraw);
}
}
Edit: Added forwarding of the WM_MOUSEWHEEL message as per MinnesotaFat's suggestion.
I have done this with TextBoxes before, but I think the solution should work for you as well:
// Get/Set Scroll positions of a control handle
private unsafe Win32.POINT GetScrollPos(System.IntPtr myHandle)
{
Win32.POINT res = new Win32.POINT();
IntPtr ptr = new IntPtr(&res);
Win32.SendMessage(myHandle, Win32.EM_GETSCROLLPOS, 0, ptr);
return res;
}
private unsafe void SetScrollPos(Win32.POINT point, System.IntPtr myHandle)
{
IntPtr ptr = new IntPtr(&point);
Win32.SendMessage(myHandle, Win32.EM_SETSCROLLPOS, 0, ptr);
}
Win32 details
public const int WM_USER = 0x400;
public const int EM_GETSCROLLPOS = (WM_USER + 221);
public const int EM_SETSCROLLPOS = (WM_USER + 222);
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int x;
public int y;
}
[DllImport("user32")] public static extern int SendMessage(
HWND hwnd, int wMsg, int wParam, IntPtr lParam);
Then just attached to both of the ListView scrolled events and do something like this:
private void ListView1Scrolled(object sender, System.EventArgs e)
{
SetScrollPos(GetScrollPos(ListView1.Handle), ListView2.Handle);
}
private void ListView2Scrolled(object sender, System.EventArgs e)
{
SetScrollPos(GetScrollPos(ListView2.Handle), ListView1.Handle);
}
DoctaJonez' answer works marvellously. For completeness, if you add another condition to the if statement in the WndProc method, you can handle the mouse wheel scrolling events as well:
if (m.Msg == WM_VSCROLL || m.Msg == WM_MOUSEWHEEL)
And declare WM_MOUSEWHEEL:
private cont int WM_MOUSEWHEEL = 0x020A;
I want remove CheckBoxes where the Node.Type is 5 or 6. I use this code:
private void TvOne_DrawNode(object sender, DrawTreeNodeEventArgs e)
{
int type = (e.Node as Node).typ;
if (type == 5 || type == 6)
{
Color backColor, foreColor;
if ((e.State & TreeNodeStates.Selected) == TreeNodeStates.Selected)
{
backColor = SystemColors.Highlight;
foreColor = SystemColors.HighlightText;
}
else if ((e.State & TreeNodeStates.Hot) == TreeNodeStates.Hot)
{
backColor = SystemColors.HotTrack;
foreColor = SystemColors.HighlightText;
}
else
{
backColor = e.Node.BackColor;
foreColor = e.Node.ForeColor;
}
using (SolidBrush brush = new SolidBrush(backColor))
{
e.Graphics.FillRectangle(brush, e.Node.Bounds);
}
TextRenderer.DrawText(e.Graphics, e.Node.Text, this.TvOne.Font,
e.Node.Bounds, foreColor, backColor);
if ((e.State & TreeNodeStates.Focused) == TreeNodeStates.Focused)
{
ControlPaint.DrawFocusRectangle(e.Graphics, e.Node.Bounds,
foreColor, backColor);
}
e.DrawDefault = false;
}
else
{
e.DrawDefault = true;
}
}
The Problem is that then the Image and the Line to the Root Node is not there.
How can Remove the Checkbox and let the Image and the Line there?
This is wrong!
In the code you've shown, you are handling the drawing yourself for all of the nodes whose type is either 5 or 6. For the rest of the types, you're simply allowing the system to draw the nodes in the default way. That's why they all have the lines as expected, but the ones you're owner-drawing do not: You forgot to draw in the lines! You see, when you say e.DrawDefault = false; it's assumed that you really do mean it. None of the regular drawing is done, including the standard lines.
You'll either need to draw in those lines yourself, or figure out how to get by without owner-drawing at all.
From the code you have now, it looks like you're trying to simulate the system's native drawing style as much as possible in your owner-draw code, so it's not clear to me what exactly you accomplish by owner-drawing in the first place. If you're just trying to keep checkboxes from showing up for type 5 and 6 nodes (which, like the lines, are simply not getting drawn because you aren't drawing them!), there's a simpler way to do that without involving owner drawing.
So, you ask, what is that simpler way to hide the checkboxes for individual nodes? Well, it turns out that the TreeView control itself actually supports this, but that functionality is not exposed in the .NET Framework. You need to P/Invoke and call the Windows API to get at it. Add the following code to your form class (make sure you've added a using declaration for System.Runtime.InteropServices):
private const int TVIF_STATE = 0x8;
private const int TVIS_STATEIMAGEMASK = 0xF000;
private const int TV_FIRST = 0x1100;
private const int TVM_SETITEM = TV_FIRST + 63;
[StructLayout(LayoutKind.Sequential, Pack = 8, CharSet = CharSet.Auto)]
private struct TVITEM
{
public int mask;
public IntPtr hItem;
public int state;
public int stateMask;
[MarshalAs(UnmanagedType.LPTStr)]
public string lpszText;
public int cchTextMax;
public int iImage;
public int iSelectedImage;
public int cChildren;
public IntPtr lParam;
}
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam,
ref TVITEM lParam);
/// <summary>
/// Hides the checkbox for the specified node on a TreeView control.
/// </summary>
private void HideCheckBox(TreeView tvw, TreeNode node)
{
TVITEM tvi = new TVITEM();
tvi.hItem = node.Handle;
tvi.mask = TVIF_STATE;
tvi.stateMask = TVIS_STATEIMAGEMASK;
tvi.state = 0;
SendMessage(tvw.Handle, TVM_SETITEM, IntPtr.Zero, ref tvi);
}
All of the messy stuff at the top are your P/Invoke declarations. You need a handful of constants, the TVITEM structure that describes the attributes of a treeview item, and the SendMessage function. At the bottom is the function you'll actually call to do the deed (HideCheckBox). You simply pass in the TreeView control and the particular TreeNode item from which you want to remove the checkmark.
So you can remove the checkmarks from each of the child nodes to get something that looks like this:
Using TreeViewExtensions.
Usage sample:
private void MyForm_Load(object sender, EventArgs e)
{
this.treeview1.DrawMode = TreeViewDrawMode.OwnerDrawText;
this.treeview1.DrawNode += new DrawTreeNodeEventHandler(arbolDependencias_DrawNode);
}
void treeview1_DrawNode(object sender, DrawTreeNodeEventArgs e)
{
if (e.Node.Level == 1) e.Node.HideCheckBox();
e.DrawDefault = true;
}
Here is the answer's code as an Extension method, using this you can do:
public static class TreeViewExtensions
{
private const int TVIF_STATE = 0x8;
private const int TVIS_STATEIMAGEMASK = 0xF000;
private const int TV_FIRST = 0x1100;
private const int TVM_SETITEM = TV_FIRST + 63;
[StructLayout(LayoutKind.Sequential, Pack = 8, CharSet = CharSet.Auto)]
private struct TVITEM
{
public int mask;
public IntPtr hItem;
public int state;
public int stateMask;
[MarshalAs(UnmanagedType.LPTStr)]
public string lpszText;
public int cchTextMax;
public int iImage;
public int iSelectedImage;
public int cChildren;
public IntPtr lParam;
}
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam,
ref TVITEM lParam);
/// <summary>
/// Hides the checkbox for the specified node on a TreeView control.
/// </summary>
public static void HideCheckBox(this TreeNode node)
{
TVITEM tvi = new TVITEM();
tvi.hItem = node.Handle;
tvi.mask = TVIF_STATE;
tvi.stateMask = TVIS_STATEIMAGEMASK;
tvi.state = 0;
SendMessage(node.TreeView.Handle, TVM_SETITEM, IntPtr.Zero, ref tvi);
}
}
This is very good! The only modification I'd make is to pass only the TreeNode and not the TreeView to the HideCheckBox method. The TreeView can be retrieved from the TreeNode itself:
TreeView tvw = node.TreeView;