Custom ComboBox not closing properly - c#

I am trying to make my custom ComboBox inheriting from ContainerControl. I used this article as a base but rewrote it, but I use a ToolStripControlHost, my own custom ListBox & a ToolStripDropDown.
Now the ComboBox is a button where you click on to show the DropDowncontaining my ListBox, works fine with overriding OnMouseClick.
The problems starts when I try to close the DropDown, with the DropDown's 'AutoClose' property to true, the DropDown closes if you click somewhere outside the DropDown (including the button) ...
protected override void OnMouseClick(MouseEventArgs e)
{
base.OnMouseClick(e);
/* listboxControl = ToolStripDropDown */
if (!listboxControl.Visible)
{
listboxControl.Show(this, GetDropLocation(), ToolStripDropDownDirection.BelowRight);
//listbox.Capture = true;
}
}
This is the code for the click on the button .. so what happens if you click it ?
If the DropDown is shown, it first closes the DropDown, then it fires the OnMouseClick event. Meaning: listboxControl.Visible is already false & it will show the DropDown again. All of this causing a quick close-open.
I have been stuck with this problem for some time now and google doesn't seem to know a lot about this subject (that article on CodeProject has the same bug).
What I have tried is disabling AutoClose and capturing the mouse after I show the DropDown, this works partially but it affects the working of my hosted ListBox. The ListBox contains a set of controls (the items), these items have a hover paint effect. Capturing the mouse in the ListBox control prevents the OnMouseEnter to be fired.
All input would be greatly appreciated !

You need a variable to track the cursor position when the DropDown is closing.
Here is a quick and dirty example control:
public class CustomDropBox : Control {
private ListBox box = new ListBox() { IntegralHeight = false };
private ToolStripControlHost host;
private ToolStripDropDown drop;
private bool wasShowing = false;
public CustomDropBox() {
box.MinimumSize = new Size(120, 120);
box.MouseUp += box_MouseUp;
box.KeyPress += box_KeyPress;
box.Items.AddRange(new string[] { "aaa", "bbb", "ccc" });
host = new ToolStripControlHost(box) { Padding = Padding.Empty };
drop = new ToolStripDropDown() { Padding = Padding.Empty };
drop.Closing += drop_Closing;
drop.Items.Add(host);
}
private Rectangle GetDownRectangle() {
return new Rectangle(this.ClientSize.Width - 16, 0, 16, this.ClientSize.Height);
}
void drop_Closing(object sender, ToolStripDropDownClosingEventArgs e) {
if (e.CloseReason == ToolStripDropDownCloseReason.AppClicked) {
wasShowing = GetDownRectangle().Contains(this.PointToClient(Cursor.Position));
} else {
wasShowing = false;
}
}
void box_KeyPress(object sender, KeyPressEventArgs e) {
if (e.KeyChar == (char)Keys.Enter && box.SelectedIndex > -1) {
drop.Close();
}
}
void box_MouseUp(object sender, MouseEventArgs e) {
int index = box.IndexFromPoint(e.Location);
if (index > -1) {
drop.Close();
}
}
protected override void OnMouseDown(MouseEventArgs e) {
if (e.Button == MouseButtons.Left && GetDownRectangle().Contains(e.Location)) {
if (wasShowing) {
wasShowing = false;
} else {
drop.Show(this, new Point(0, this.Height));
}
}
base.OnMouseDown(e);
}
protected override void OnPaint(PaintEventArgs e) {
e.Graphics.Clear(Color.White);
ControlPaint.DrawComboButton(e.Graphics, GetDownRectangle(), ButtonState.Normal);
base.OnPaint(e);
}
}

Related

Display an alert message if a checkbox is uncheck C#

I'm trying to create a design form in visual studio with 4 checkboxes and I really want to make the user to check only one of them, and if he's not checked one, when he will press a button, he should receive a notification with the obligation to select a box, and the program should not starting.
RadioGroup is a control very similar in appearance to CheckBox. It's used to select only one RadioGroup in each group. You can define groups of radio buttons puttin inside a container (a Form, a Panel, a GroupBox). Add 4 radio buttons to your form, set the Text property.
You can check if a radio button is selected:
var isChecked = radioButton1.Checked;
Or make a method like this:
private int GetSelectedRadioIndex()
{
var buttons = new[]
{
this.radioButton1,
this.radioButton2,
this.radioButton3,
this.radioButton4
};
for (int i = 0; i < buttons.Length; i++)
{
if (buttons[i].Checked)
{
return i;
}
}
return -1;
}
If you get a <0 index, there aren't a radio selected. In other case you have a 0 index of the radio that is selected.
As indicated, use a container like a Panel or GroupBox while with a GroupBox you can set a caption to indicate what the RadioButtons are for.
Create a private list in the form
private List<RadioButton> _radioButtons;
Subscribe to the Form's OnShown event, add the following code where OptionsGroupBox is a GroupBox with four Radio Buttons. This ensures no default selection which is optional.
private void OnShown(object sender, EventArgs e)
{
_radioButtons = OptionsGroupBox.Controls.OfType<RadioButton>().ToList();
_radioButtons.ForEach(rb => rb.Checked = false);
}
Add a button to assert/get their selection.
private void CheckSelectionButton_Click(object sender, EventArgs e)
{
var selection = _radioButtons.FirstOrDefault(x => x.Checked);
if (selection == null)
{
MessageBox.Show("Make a selection");
}
else
{
MessageBox.Show($"You selected {selection.Text}");
}
}
Edit: Working with both Panel and GroupBox
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
namespace RadioButtonApp
{
public partial class Form1 : Form
{
private List<RadioButton> _radioButtonsGroupBox;
private List<RadioButton> _radioButtonsPanel;
public Form1()
{
InitializeComponent();
Shown += OnShown;
}
private void OnShown(object sender, EventArgs e)
{
_radioButtonsGroupBox = OptionsGroupBox.Controls.OfType<RadioButton>().ToList();
_radioButtonsGroupBox.ForEach(rb => rb.Checked = false);
_radioButtonsPanel = OptionsPanel.Controls.OfType<RadioButton>().ToList();
_radioButtonsPanel.ForEach(rb => rb.Checked = false);
}
private void CheckSelectionInGroupBoxButton_Click(object sender, EventArgs e)
{
var selection = _radioButtonsGroupBox.FirstOrDefault(x => x.Checked);
if (selection == null)
{
MessageBox.Show("Make a selection");
}
else
{
MessageBox.Show($"You selected {selection.Text}");
}
}
private void CheckSelectionInPanelButton_Click(object sender, EventArgs e)
{
var selection = _radioButtonsPanel.FirstOrDefault(x => x.Checked);
if (selection == null)
{
MessageBox.Show("Make a selection");
}
else
{
MessageBox.Show($"You selected {selection.Text}");
}
}
}
}

WinForms User Control has ComboBox that causes ToolStripDropDown to auto-close

I have a custom WinForms user control that looks like a combobox but instead opens a ToolStripDropDown that contains another custom user control, called NumericFilterPanel, that has a checkbox, a combobox, and a textbox.
The problem is that when the user click-selects an option for the combobox embedded in the dropdown control, it causes the parent dropdown to hide.
I have set ToolStripDropDown.AutoClose = false, which fixes the original problem, but now I am having difficulty detecting all the situations where the dropdown loses focus, such as when the user clicks on the parent form or switches programs. Sometimes the dropdown remains visible and topmost.
Is there a way to either keep AutoClose = true and prevent the embedded combobox from closing the parent dropdown, or is there a way to always detect when the dropdown has lost focus so I can manually close it?
using System;
using System.Drawing;
using System.Windows.Forms;
namespace mviWinControls
{
public partial class NumericRangeDropDown : UserControl
{
private const int ARROW_HEIGHT = 4;
private Brush arrowBrush = new SolidBrush(Color.FromArgb(77, 97, 133));
private ToolStripDropDown _dropdown;
private ToolStripControlHost _host;
private NumericFilterPanel _filter;
public NumericRangeDropDown()
{
InitializeComponent();
_filter = new NumericFilterPanel();
_filter.DropDown = this;
_host = new ToolStripControlHost(_filter);
_host.Margin = Padding.Empty;
_host.Padding = Padding.Empty;
_dropdown = new ToolStripDropDown();
_dropdown.Margin = Padding.Empty;
_dropdown.Padding = Padding.Empty;
_dropdown.AutoClose = false; // Use this because panel has a combobox. https://social.msdn.microsoft.com/Forums/windows/en-US/dd95b982-820e-4807-8a1f-79c74acab3f8/two-problems-toolstripdropdown?forum=winforms
_dropdown.Items.Add(_host);
_dropdown.Leave += new System.EventHandler(this.DropDown_Leave);
this.Leave += new System.EventHandler(this.DropDown_Leave);
}
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (components != null) components.Dispose();
if (_dropdown != null) _dropdown.Dispose();
}
base.Dispose(disposing);
}
public override string Text
{
get
{
return base.Text;
}
set
{
base.Text = value;
_filter.SetValue(value);
}
}
protected override void OnPaint(PaintEventArgs e)
{
//base.OnPaint(e);
TextBox _txtDraw = new TextBox();
_txtDraw.Width = this.Width;
using (Bitmap bmp = new Bitmap(_txtDraw.Width, _txtDraw.Height))
{
_txtDraw.DrawToBitmap(bmp, new Rectangle(0, 0, _txtDraw.Width, _txtDraw.Height));
e.Graphics.DrawImage(bmp, 0, 0);
}
StringFormat format = new StringFormat();
format.Alignment = StringAlignment.Near;
format.FormatFlags = StringFormatFlags.NoWrap;
format.LineAlignment = StringAlignment.Center;
using (Brush b = new SolidBrush(this.ForeColor))
e.Graphics.DrawString(this.Text, this.Font, b, this.DisplayRectangle, format);
Point[] arrowPoints = new Point[] { new Point(this.Width - ARROW_HEIGHT * 3 - 2, (this.Height - ARROW_HEIGHT) / 2),
new Point(this.Width - ARROW_HEIGHT + 1 - 2, (this.Height - ARROW_HEIGHT) / 2),
new Point(this.Width - ARROW_HEIGHT * 2 - 2, this.Height - (this.Height - ARROW_HEIGHT) / 2) };
e.Graphics.FillPolygon(arrowBrush, arrowPoints );
}
private void DropDown_Leave(object sender, EventArgs e)
{
HideDropDown();
this.Text = _filter.SummaryText();
}
private void NumericRangeDropDown_Click(object sender, EventArgs e)
{
if (_dropdown.Visible)
HideDropDown();
else
ShowDropDown();
}
public void ShowDropDown()
{
_dropdown.Show(this, new Point(0, this.Height), ToolStripDropDownDirection.Default);
_dropdown.BringToFront();
//_dropdown.Focus();
_filter.Select();
_filter.Focus();
}
public void HideDropDown()
{
_dropdown.Close();
this.Invalidate();
}
}
}
Here's a combobox that can automatically disable and enable the AutoClose property on the host control for you.
Source(I modified it for a combobox versus the DatePicker in their example): http://www.queasy.me/programming/questions/13919634/tool+strip+toolstripdropdownbutton+close+and+lose+window+focus
public partial class CComboBox : ComboBox
{
private bool savedAutoClose;
public CComboBox()
{
InitializeComponent();
}
protected override void OnDropDownClosed(EventArgs e)
{
if (this.Parent != null)
{
var dropDownHost = this.Parent.Parent as ToolStripDropDown; // recursive instead?
if (dropDownHost != null)
dropDownHost.AutoClose = savedAutoClose; // restore the parent's AutoClose preference
}
base.OnDropDownClosed(e);
}
protected override void OnDropDown(EventArgs e)
{
if (this.Parent != null)
{
var dropDownHost = this.Parent.Parent as ToolStripDropDown; // recursive instead?
if (dropDownHost != null)
{
savedAutoClose = dropDownHost.AutoClose;
// ensure that our parent doesn't close while the calendar is open
dropDownHost.AutoClose = false;
}
}
base.OnDropDown(e);
}
}
Having taken a good look at the source code, the bug (and it is a bug) lies in the fact that ToolStripManager, which sets up a message filter to catch mouse-clicks outside the active ToolStrip, checks if the click is within the bounds of a child window.
The problem is that it uses activeToolStrip.ClientRectangle to verify this, and does not check child windows of the window that was clicked. In the case of a ComboBox, the drop-down is a separate child window which floats above everything, and can actually be out of the bounds of the main combo window if the drop-down is large.
The relevant line is:
if (!activeToolStrip.ClientRectangle.Contains(pt.x, pt.y)) {
I have found another solution to temporarily disable the automatic close while the dropdown is open.
Ideally, you are supposed to use a ToolStripComboBox within a ToolStrip rather than just a bare ComboBox. However if you would like to just us a bare one, you can add events to call the relevant private methods to suspend and resume the message filter.
static class ToolStripComboBoxFilter
{
private static Action SuspendMenuMode = (Action) typeof(ToolStripManager)
.GetNestedType("ModalMenuFilter", BindingFlags.NonPublic)
.GetMethod(nameof(SuspendMenuMode), BindingFlags.NonPublic | BindingFlags.Static)
.CreateDelegate(typeof(Action));
private static Action ResumeMenuMode = (Action)typeof(ToolStripManager)
.GetNestedType("ModalMenuFilter", BindingFlags.NonPublic)
.GetMethod(nameof(ResumeMenuMode), BindingFlags.NonPublic | BindingFlags.Static)
.CreateDelegate(typeof(Action));
public static void AddToolStripFilterEvents(this ComboBox combo)
{
combo.DropDown += OnDropDown;
combo.DropDownClosed += OnDropDownClosed;
}
private static void OnDropDown(object sender, EventArgs e)
{
SuspendMenuMode();
}
private static void OnDropDownClosed(object sender, EventArgs e)
{
ResumeMenuMode();
}
}
You can use it like this
myComboBox.AddToolStripFilterEvents();

C# - Drag MenuStrip item to ListBox

I want to allow user to drag any item from MenuStrip to a ListBox.
I did it between to ListBoxes, but can not do it with MenuStrip.
Thanks a lot for your help.
I use WinForms, C#
For the destination ListBox I modified its property
this.listBox2.AllowDrop = true;
and created the following two events:
private void listBox2_DragOver(
object sender, System.Windows.Forms.DragEventArgs e)
{
e.Effect=DragDropEffects.All;
}
private void listBox2_DragDrop(
object sender, System.Windows.Forms.DragEventArgs e)
{
if(e.Data.GetDataPresent(DataFormats.StringFormat))
{
string str= (string)e.Data.GetData(
DataFormats.StringFormat);
listBox2.Items.Add(str);
}
}
What I need is what should be done to the source MenuStrip to allow drag items from it the ListBox, in over words how to make MenuStrip draggable.
Thanks to all for their help.
I found the solution:
The missing event is that I should add event to ToolStripMenuItem_MouseDown, I prefer to use right click instead of left click to avoid the conflict between ToolStripMenuItem_Click and the drag event, this the code:
AllowDrop = true;
private void tsmi_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button == System.Windows.Forms.MouseButtons.Right)
DoDragDrop(sender, System.Windows.Forms.DragDropEffects.Copy);
}
Add also this code to the ListView:
private void lvAllowDropListView_DragDrop(object sender, System.Windows.Forms.DragEventArgs e)
{
System.Windows.Forms.ToolStripMenuItem button = e.Data.GetData(typeof(System.Windows.Forms.ToolStripMenuItem))
as System.Windows.Forms.ToolStripMenuItem;
if (button != null)
{
try
{
SmallImageList = sysIcons.SmallIconsImageList;
LargeImageList = sysIcons.LargeIconsImageList;
System.Windows.Forms.ToolStripMenuItem item = e.Data.GetData(typeof(System.Windows.Forms.ToolStripMenuItem))
as System.Windows.Forms.ToolStripMenuItem;
if (item != null)
{
AddToolStripMenuItem(item.Text, item.Name);
}
}
catch { }
}
}
private void AddToolStripMenuItem(string name, string tag)
{
System.Windows.Forms.ListViewItem item = new System.Windows.Forms.ListViewItem(name);
int Index = -1;
for (int i = 0; i < Items.Count;i++ )
if(Items[i].Tag.ToString() == tag)
{
Index = i;
break;
}
if (Index == -1)
{
item.Tag = tag;
Items.Add(item);
}
}
Drag Menu Strip Item is the same like ListBox item.
Check your code...

Move Item from one cell to another

I have a tableLayoutPanel with 16 cells. 15 of the cells have controls. I want to be able to move the controls from one cell to another at runtime.
I have used
private void button15_Click(object sender, EventArgs e)
{
tableLayoutPanel1.Controls.Remove(button15);
tableLayoutPanel1.Controls.Add(button15, 3, 3);
}
This works well but i want to know if there is any better way to do this???
In Winforms, you can only move a control inside its parent (of course there are some exceptions to some controls which in fact don't have any Parent). So the idea here is if you want to move a control of your TableLayoutPanel, you have to set its Parent to your Form of another container when mouse is held down, when moving, the position of the control is in the new parent, after mouse is released, we have to set the Parent of the control to the TableLayoutPanel back, of course we have to find the drop-down cell position and use SetCellPosition method to position the control on the TableLayoutPanel, here is the demo code for you (works great), I use 2 Buttons in this demo, you can replace them with any control you want:
public partial class Form1 : Form {
public Form1() {
InitializeComponent();
//This will prevent flicker
typeof(TableLayoutPanel).GetProperty("DoubleBuffered", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).SetValue(tableLayoutPanel1, true, null);
}
Point downPoint;
bool moved;
//This is used to store the CellBounds together with the Cell position
//so that we can find the Cell position later (after releasing mouse).
Dictionary<TableLayoutPanelCellPosition, Rectangle> dict = new Dictionary<TableLayoutPanelCellPosition, Rectangle>();
//MouseDown event handler for all your controls (on the tableLayoutPanel1)
private void Buttons_MouseDown(object sender, MouseEventArgs e) {
Control button = sender as Control;
button.Parent = this;
button.BringToFront();
downPoint = e.Location;
}
//MouseMove event handler for all your controls (on the tableLayoutPanel1)
private void Buttons_MouseMove(object sender, MouseEventArgs e) {
Control button = sender as Control;
if (e.Button == MouseButtons.Left) {
button.Left += e.X - downPoint.X;
button.Top += e.Y - downPoint.Y;
moved = true;
tableLayoutPanel1.Invalidate();
}
}
//MouseUp event handler for all your controls (on the tableLayoutPanel1)
private void Buttons_MouseUp(object sender, MouseEventArgs e) {
Control button = sender as Control;
if (moved) {
SetControl(button, e.Location);
button.Parent = tableLayoutPanel1;
moved = false;
}
}
//This is used to set the control on the tableLayoutPanel after releasing mouse
private void SetControl(Control c, Point position) {
Point localPoint = tableLayoutPanel1.PointToClient(c.PointToScreen(position));
var keyValue = dict.FirstOrDefault(e => e.Value.Contains(localPoint));
if (!keyValue.Equals(default(KeyValuePair<TableLayoutPanelCellPosition, Rectangle>))) {
tableLayoutPanel1.SetCellPosition(c, keyValue.Key);
}
}
//CellPaint event handler for your tableLayoutPanel1
private void tableLayoutPanel1_CellPaint(object sender, TableLayoutCellPaintEventArgs e) {
dict[new TableLayoutPanelCellPosition(e.Column, e.Row)] = e.CellBounds;
if (moved) {
if (e.CellBounds.Contains(tableLayoutPanel1.PointToClient(MousePosition))) {
e.Graphics.FillRectangle(Brushes.Yellow, e.CellBounds);
}
}
}
}
Remove Lock & set dock to none and move!

How to prevent tooltip from flickering in custom control?

I have made a custom control and when a condition is met, I want to show a tooltip:
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
var plannedItem = GetPlannedItemByPosition(e.Location);
if (plannedItem != null)
_tooltip.SetToolTip(this, plannedItem.Description);
else
_tooltip.RemoveAll();
}
This code works fine, excepts for the face that the tooltip flickers.
This custom control, paints all the information in the OnPaint event, maybe this has something to do with it? And if it does, how can I prevent the tooltip from flickering?
Remember last mouse position and set the tooltip only when the mouse position changes.
public partial class Form1 : Form
{
private int lastX;
private int lastY;
private void button1_MouseMove(object sender, MouseEventArgs e)
{
if (e.X != this.lastX || e.Y != this.lastY)
{
toolTip1.SetToolTip(button1, "test");
this.lastX = e.X;
this.lastY = e.Y;
}
}
This will happen when you display the tooltip at the mouse cursor position. As soon as the tip window shows up, Windows notices that the mouse is located in that window and posts a MouseMove message. Which makes the tooltip disappear. Which makes Windows send a MouseMove message to your control, running your OnMouseMove() method. Which makes the tooltip appear again. Etcetera, you'll see the tooltip rapidly flickering.
Solve this by any of the following methods:
show the tooltip well away from the mouse position so it won't overlap the mouse cursor
only update/show the tooltip when it needs to be changed
set the control's Capture property to true so the tooltip won't get a MouseMove message
Since this is a painted custom control, I think it might be easier to just have a variable hold the last shown tip, and instead of always "setting" the tooltip, just show it.
Simple example (using just a form):
public partial class Form1 : Form {
private List<TipRect> _Tips = new List<TipRect>();
private TipRect _LastTip;
private ToolTip _tooltip = new ToolTip();
public Form1() {
InitializeComponent();
_Tips.Add(new TipRect(new Rectangle(32, 32, 32, 32), "Tip #1"));
_Tips.Add(new TipRect(new Rectangle(100, 100, 32, 32), "Tip #2"));
}
private void Form1_Paint(object sender, PaintEventArgs e) {
foreach (TipRect tr in _Tips)
e.Graphics.FillRectangle(Brushes.Red, tr.Rect);
}
private void Form1_MouseMove(object sender, MouseEventArgs e) {
TipRect checkTip = GetTip(e.Location);
if (checkTip == null) {
_LastTip = null;
_tooltip.Hide(this);
} else {
if (checkTip != _LastTip) {
_LastTip = checkTip;
_tooltip.Show(checkTip.Text, this, e.Location.X + 10, e.Location.Y + 10, 1000);
}
}
}
private TipRect GetTip(Point p) {
TipRect value = null;
foreach (TipRect tr in _Tips) {
if (tr.Rect.Contains(p))
value = tr;
}
return value;
}
}
Here is the TipRect class I created to simulate whatever your PlannedItem class is:
public class TipRect {
public Rectangle Rect;
public string Text;
public TipRect(Rectangle r, string text) {
Rect = r;
Text = text;
}
}
I imagine your mouse does move a little when you think it is still. I suggest you do some kind of caching here - only call _tooltip.SetToolTip if the plannedItem has changed.
For the visitors of this thread, here is what I did, following suggestions above (VB.NET):
Dim LastToolTip As String
Private Sub PictureBox1_MouseMove(sender As Object, e As MouseEventArgs) Handles PictureBox1.MouseMove
Dim NewToolTip = CalculateTooltipText(e.X, e.Y)
If LastToolTip <> NewToolTip Then
ToolTip1.SetToolTip(PictureBox1, NewToolTip)
LastToolTip = NewToolTip
End If
End Sub
It stopped the flickering.
c# (works on tooltip chart):
Point mem = new Point();
private void xxx_MouseMove(MouseEventArgs e){
// start
Point pos = e.Location;
if (pos == mem) { return; }
// your code here
// end
mem = pos
}

Categories

Resources