Allow a ListBox to overlap a TableLayoutPanel (C# .NET) - c#

I have a form that contains a TableLayoutPanel with various controls and labels in it. One of them is a custom control that inherits from ComboBox that has extra auto-complete behavior (auto-completes on any text rather than just left to right). I didn't write the code for this control, so I'm not super familiar with how it works, but essentially upon clicking on the Combobox, it adds a ListBox below the ComboBox, within the same Panel of the TableLayoutPanel, that covers the normal drop down.
Unfortunately, the TableLayoutPanel prevents the ListBox from being fully visible when added, and only one item is shown. The goal is to get it to look like a normal ComboBox which would drop down to cover any controls below it.
Is there any way to allow a control that is in a TableLayoutPanel to overlap the TableLayoutPanel to get this to work as I want? I want to avoid any controls moving around due to the TableLayoutPanel growing to accommodate the ListBox.
Relevant code from the control:
void InitListControl()
{
if (listBoxChild == null)
{
// Find parent - or keep going up until you find the parent form
ComboParentForm = this.Parent;
if (ComboParentForm != null)
{
// Setup a messaage filter so we can listen to the keyboard
if (!MsgFilterActive)
{
Application.AddMessageFilter(this);
MsgFilterActive = true;
}
listBoxChild = listBoxChild = new ListBox();
listBoxChild.Visible = false;
listBoxChild.Click += listBox1_Click;
ComboParentForm.Controls.Add(listBoxChild);
ComboParentForm.Controls.SetChildIndex(listBoxChild, 0); // Put it at the front
}
}
}
void ComboListMatcher_TextChanged(object sender, EventArgs e)
{
if (IgnoreTextChange > 0)
{
IgnoreTextChange = 0;
return;
}
InitListControl();
if (listBoxChild == null)
return;
string SearchText = this.Text;
listBoxChild.Items.Clear();
// Don't show the list when nothing has been typed
if (!string.IsNullOrEmpty(SearchText))
{
foreach (string Item in this.Items)
{
if (Item != null && Item.ToLower().Contains(SearchText.ToLower()))
{
listBoxChild.Items.Add(Item);
listBoxChild.SelectedIndex = 0;
}
}
}
if (listBoxChild.Items.Count > 0)
{
Point PutItHere = new Point(this.Left, this.Bottom);
Control TheControlToMove = this;
PutItHere = this.Parent.PointToScreen(PutItHere);
TheControlToMove = listBoxChild;
PutItHere = ComboParentForm.PointToClient(PutItHere);
TheControlToMove.Anchor = ((System.Windows.Forms.AnchorStyles)
((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right)));
TheControlToMove.BringToFront();
TheControlToMove.Show();
TheControlToMove.Left = PutItHere.X;
TheControlToMove.Top = PutItHere.Y;
TheControlToMove.Width = this.Width;
int TotalItemHeight = listBoxChild.ItemHeight * (listBoxChild.Items.Count + 1);
TheControlToMove.Height = Math.Min(ComboParentForm.ClientSize.Height - TheControlToMove.Top, TotalItemHeight);
}
else
HideTheList();
}
Images:
Desired behavior
Current behavior

Going on the suggestion from TaW, I came up with a tentative solution. This form isn't re-sizable but does auto-size so that it looks ok if the user changes their DPI in Windows.
To resolve this, I moved the control out of the TableLayoutPanel to an arbitrary position in the Parent of the TableLayoutPanel. On form loading, I summed the coordinates of the TableLayoutPanel and an empty panel in the cell that I wanted the control to be located on top of. This worked for my needs but it feels like a kludge.
The better solution is probably to use Control.PointToScreen and Control.PointToClient methods, however I wasn't able to get these methods to give me the correct coordinates.

Related

Adding panels programatically based on .top is being based on the scroll of its parent

I am looking for some assistance in fixing an issue. At the moment I have developed some code that adds a user control to a panel. It adds multiple user controls to the panel and does this based on the .top feature. However, once the panel that I am adding the user controls to is scrolled down the user controls seem to be placed strangely.
I have already tried to adjust the .top value but I am not sure how to do it in relation to the scroll of the panel.
int i = 0;
foreach(memberInformation mi in pnlMembers.Controls.OfType<memberInformation>())
{
try
{
if (UserInformation.isPartyLeader)
{
mi.canUserEdit = "true";
}
else
{
mi.canUserEdit = "false";
}
mi.playerName = downloadInfo.Split(':')[i].Split(',')[0];
mi.playerRole = downloadInfo.Split(':')[i].Split(',')[1];
}
catch
{
pnlMembers.Controls.Remove(mi);
}
i++;
}
VIDEO to show what is happening: https://gyazo.com/985566afb7e4bab464dd06da191a0710
https://gyazo.com/4b0514cbdb310ea8abc46a397458130c
The correct way in order to position panels inside another panel is to use flowlayoutpanel.
Thanks

Bring a control to front

I have a user control that uses a textbox and a list box. List box isn't visible, it only becomes visible when user starts typing or click in text box.
I have added the user control to a group box which is on the form.
Now when the listox becomes visible, it stays inside the group box, and can't see the full height. I wan't it float on top so that i can see the full height.
I have looked around, implemented some solutions but nothing worked for me.
Constructor for the user control
namespace YarCustomControl
{
public partial class YarCustom : TextBox
{
public YarCustom()
{
InitializeComponent();
_code = "";
_id = -1;
//list box handling
listBox = new ListBox();
listBox.Visible = false;
listBox.Font = this.Font;
listBox.Location = this.Location;
listBox.BorderStyle = BorderStyle.Fixed3D;
listBox.Resize += new EventHandler(listBox_Resize);
//listBox.SelectedValueChanged += new EventHandler(listBox_SelectedValueChanged);
listBox.KeyDown += new KeyEventHandler(listBox_KeyDown);
listBox.Click += new EventHandler(listBox_Click);
//test => no affect on listbox
this.Controls.Add(listBox);
listBox.Visible = false;
}
}
}
and the following method makes the listbox visible. Both SetchildIndex (commented and not commented) throw an error
private void makeListBoxVisible()
{
Form parentForm = (this.FindForm() as Form);
//parentForm.Controls.SetChildIndex(listBox, 0);
this.Controls.SetChildIndex(listBox, 0);
listBox.Visible = true;
listBox.BringToFront();
}
What is the best approach for handling something like this?
My environment is VS2010 and WinForms.
Now when the listox becomes visible, it stays inside the group box,
and can't see the full height. I wan't it float on top so that i can
see the full height.
Simply put it directly on the Form.

Implementing control by control (not smooth) scrolling panel

I have a FlowLayoutPanel that contains User Controls from up to down with vertical scrollbar.
Like any other scrollable control, I can scroll it pixel by pixel.
Is there a way (.NET Framework or native API way) to scroll it User Control by User Control, in order to snap to next or previous User Control? They can have different height.
I would like to reproduce DataGridView or Excel/Calc row by row scrolling type.
I this question - How to make scrollviewer scroll pixels not components (wpf), the behavior you expect is unwanted. Thus, you could do what the user has done in the described question.
The essential difference between your question and the one referenced is to do with host control. You have used FlowLayoutPanel and in the referenced question StackPanel is used.
So, if by any chance your application is a WPF application can you change it to StackPanel?
Did setting these properties AutoScroll=True with WrapContent=True on FlowLayoutPanel not work?
Quick and dirty solution.
Set AutoScroll=false, add a VScrollBar, and put the following code:
vScrollBar1.Maximum = MyList.VerticalScroll.Maximum;
vScrollBar1.SmallChange = MyList.VerticalScroll.SmallChange;
vScrollBar1.LargeChange = MyList.VerticalScroll.LargeChange;
vScrollBar1.Scroll += (sender, args) =>
{
switch (args.Type)
{
case ScrollEventType.ThumbTrack:
var sum = 0;
Control prevCtrl = null;
foreach (Control control in MyList.Controls)
{
if (prevCtrl == null || control.Bottom > prevCtrl.Bottom)
{
if (args.OldValue >= sum && args.OldValue < sum + control.Height)
{
MyList.AutoScrollPosition = new Point(0, sum);
}
sum += control.Height;
}
prevCtrl = control;
}
break;
}
}

How to use ctrl key + mouse click to select multiple controls?

Probably this question has already an answer here but I was not able to find it..
I have a tabControl with a flowlayoutpanel in each tab page where I can add controls at run time. I can rearrange them, move them across tab pages.. How can I select multiple controls to be able to move them around using ctrl key + mouse click?
This is my drag event so far:
private void control_DragDrop(object sender, DragEventArgs e)
{
Control target = new Control();
target.Parent = sender as Control;
if (target != null)
{
int targetIndex = FindCSTIndex(target.Parent);
if (targetIndex != -1)
{
string cst_ctrl = typeof(CustomControl).FullName;
if (e.Data.GetDataPresent(cst_ctrl))
{
Button source = new Button();
source.Parent = e.Data.GetData(cst_ctrl) as CustomControl;
if (targetIndex != -1)
fl_panel = (FlowLayoutPanel)tabControl1.SelectedTab.Controls[0];
if (source.Parent.Parent.Name == target.Parent.Parent.Parent.Name)
{
this.fl_panel.Controls.SetChildIndex(source.Parent, targetIndex);
}
else
{
target.Parent.Parent.Parent.Controls.Add(source.Parent);
this.fl_panel.Controls.SetChildIndex(source.Parent, targetIndex);
}
}
}
}
}
private int FindCSTIndex(Control cst_ctr)
{
fl_panel = (FlowLayoutPanel)tabControl1.SelectedTab.Controls[0];
for (int i = 0; i < this.fl_panel.Controls.Count; i++)
{
CustomControl target = this.fl_panel.Controls[i] as CustomControl;
if (cst_ctr.Parent == target)
return i;
}
return -1;
}
This is not an easy, nor a common task. But surely doable and depending on preconditions could become trivial without need to spend multi-man-year effort on it ^^.
You have many options:
controls support selection;
container control support children controls selection;
overlay.
Handling selection is pretty easy: have a dictionary (or a control property, possibly using Tag) to store if control is selected or not, show selection somehow, when control is Ctrl-clicked invert selection. You can even provide Shift-key selection.
As #Hans Passant commented, you can use overlay window (invisible window on top of everything) to draw selection reticle there as well as handle selection and dragging itself. Or it could be a custom control with property IsSelected, setting which will draw something (border?) to indicate selection.
Easiest option would be to create SelectionPanel control, which can host any other controls inside, has IsSelected indication and is draggable. When children is added subscribe to MouseUp/MouseDown events or you can only allow to drag if special area of SelectionPanel is clicked. To example, you could have option Enable dragging in your software, when set all SelectionPanels will display special area (header?) which you can drag or Ctrl-click.

How does one get a ListView (GridView) cell's contents when clicked with a mouse

I am trying to get the text value of a "cell" inside of a GridView that is set as the view of a ListView. I do not want to get the SelectedItem of the ListView as that just returns my entire View Model (but not which property the cell refers to).
I am able to get the text value by responding to direct mouse events (up down or whatever) and if the value is a textblock, obviously I can use the text. This works great and as of right now this is my only solution, although its currently limited.
I would like to take it a step further and be able to click anywhere with in the cell area, navigate around to find the appropriate textblock and then use that value. I have tried a half million ways to do this but what seems logical doesn't seem to quite work out like it should.
Setup:
I have a dynamic GridView that creates its own columns and bindings based on data models that I pass to it. I am using a programmatic cell template (shown below) to have individual control over the cells, particularly so I can add a "border" to it making it actually separate out each cell. I have named the objects so I can access them easier when I'm navigating around the VisualTree.
Here is the Template Code. (Note that the content presenter originally was a textblock itself, but this was changed for later flexibility)
private DataTemplate GetCellTemplate(string bindingName)
{
StringBuilder builder = new StringBuilder();
builder.Append("<DataTemplate ");
builder.Append("xmlns='http://schemas.microsoft.com/winfx/");
builder.Append("2006/xaml/presentation' ");
builder.Append("xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' ");
builder.Append("xmlns:local = 'clr-namespace:XXXXXXXX");
builder.Append(";assembly=XXXXXXXXX'>");
builder.Append("<Border Name=\"border\" BorderThickness=\"1,0,0,0\" BorderBrush=\"Gray\" Margin=\"-6,-3,-6,-3\">");
builder.Append("<Grid Margin=\"6,3,6,3\">");
builder.Append("<ContentPresenter Name=\"content\" HorizontalAlignment=\"Stretch\" Content=\"{Binding ");
builder.Append(string.Format("{0}", bindingName));
builder.Append("}\"/>");
builder.Append("</Grid>");
builder.Append("</Border>");
builder.Append("</DataTemplate>");
DataTemplate cellTemplate= (DataTemplate)XamlReader.Parse(builder.ToString());
return cellTemplate;
}
What I have Tried:
The logical approach for me was to react to a Mouse event. From the object that had the mouse event I would do either
A. Look at its children to find a textblock, or
B. Get its parent then look for child with a textblock.
My assumption is that if I click in white space I'm clicking in a container that has my textblock. So far the two things that come up are a Border and a Rectangle (if I don't click the text itself). A. Returns absolutely nothing except for the recangle and the border. When I do B i can find textblocks but they are every single text block in the entire row.
So what I try to do from that is get all textblocks, then go backwards till I find which one has a IsMouseOver property as true. It turns out none of these objects EVER have a IsMouseOver except the content presenter for the entire row. So this seems to indicate to me is that the whitespace in the cells does not actually contain the textblock.
What I find is that when I click on the Border and start looking at children, I eventually get to a container that has a rectangle (the rectangle I click) and a grid row view presenter. The presenter shows all of the objects inside the row (hence why i would get all textblocks when i do this recursive scan).
Here is some of the code used to do this to get an idea of what i'm doing. I have written about 10 different versions of this same recursive code generally attempting to find who has the Mouse over it and is related to a textbox.
private void OnPreviewMouseUp(object sender, MouseButtonEventArgs e)
{
object original = e.OriginalSource;
if (original is TextBlock)
{
this.valueTextBlock.Text = ((TextBlock)original).Text;
}
else if (original is FrameworkElement)
{
var result = GetAllNestedChildren<Border>(VisualTreeHelper.GetParent((DependencyObject)original)).Where(x => x.Name == "border").Where(x => HasAChildWithMouse(x)).ToList();
}
else
{
this.valueTextBlock.Text = string.Empty;
}
}
private bool HasAChildWithMouse(UIElement element)
{
if (element.IsMouseOver || element.IsMouseDirectlyOver)
return true;
var childCount = VisualTreeHelper.GetChildrenCount(element);
for (int i = 0; i < childCount; ++i)
{
var child = VisualTreeHelper.GetChild(element, i);
if (child is UIElement)
if (HasAChildWithMouse((UIElement)child))
return true;
}
return false;
}
private IEnumerable<T> GetAllNestedChildren<T>(DependencyObject obj) where T : UIElement
{
if (obj is T)
yield return obj as T;
var childCount = VisualTreeHelper.GetChildrenCount(obj);
for (int i = 0; i < childCount; ++i)
{
var child = VisualTreeHelper.GetChild(obj, i);
foreach (var nested in GetAllNestedChildren<T>(child))
yield return nested;
}
}
private T GetObjectByTypeParentHasMouse<T>(DependencyObject obj) where T : UIElement
{
if (obj is T)
{
if ((VisualTreeHelper.GetParent(obj) as UIElement).IsMouseOver )
{
return obj as T;
}
}
var childCount = VisualTreeHelper.GetChildrenCount(obj);
for (int i = 0; i < childCount; ++i)
{
var child = VisualTreeHelper.GetChild(obj, i);
var correctType = GetObjectByTypeParentHasMouse<T>(child);
if (correctType != null)
return correctType;
}
return null;
}
private T GetContainedType<T>(DependencyObject obj, bool checkForMouseOver) where T : UIElement
{
if (obj is T && ((T)obj).IsMouseOver)
return obj as T;
var childCount = VisualTreeHelper.GetChildrenCount(obj);
for (int i = 0; i < childCount; ++i)
{
var child = VisualTreeHelper.GetChild(obj, i);
var correctType = GetContainedType<T>(child, checkForMouseOver);
if (correctType != null)
return correctType;
}
return null;
}
The other approach I took was to start with the TextBlock itself, find its containing parent and find out how i can navigate to the answer. I find the templateparent is the ContentPresenter (named ="content") I find the grid, and then the border. The parent of the border is a content presenter whos content is the data view model for the entire row. The parent of this contentpresenter is the grid column's presenter. This is the same one that i was navigating up to in the other one.
It would appear that the first approach objects while are contain the cell do not actually contain the textblock or the entire cell templated items. It would appear to me there is no way to go from the Border or Rectangle that is clicked, back to the actual text field.
"Long story short" is there ANY way to make this connection?
(Btw I am not willing to give up this ListView/GridView because its payoffs far outweigh this negative and I'd gladly give up on this idea to keep the rest).
I think you sjould be able to either
1) Add some kind of (toggle)button to the root of your data template, and either bind to Command and handle it on your viewmodel or bind to IsChecked/IsPressed and handle changes via data triggers or w/e on the view side.
2) Add EventTrigger to your datatemplate at some point, and handle PreviewNouseUp/Down events there via simple animations.

Categories

Resources