Custom panel with layout engine - c#

I'm trying to create a custom Panel control with my own layout engine.
I need every control that is added to my panel to be added below and to take full width (-padding), like below:
Below is my code:
using System.Drawing;
using System.Windows.Forms;
using System.Windows.Forms.Layout;
namespace VContainer
{
internal class VerticalFillList : Panel
{
public VerticalFillList()
{
AutoScroll = true;
MinimumSize = new Size(200, 200);
Size = new Size(200, 300);
Padding = new Padding(10);
}
private readonly VerticalFillLayout _layoutEngine = new VerticalFillLayout();
public override LayoutEngine LayoutEngine
{
get { return _layoutEngine; }
}
private int _space = 10;
public int Space
{
get { return _space; }
set
{
_space = value;
Invalidate();
}
}
}
internal class VerticalFillLayout : LayoutEngine
{
public override bool Layout(object container, LayoutEventArgs layoutEventArgs)
{
var parent = container as VerticalFillList;
Rectangle parentDisplayRectangle = parent.DisplayRectangle;
Point nextControlLocation = parentDisplayRectangle.Location;
foreach (Control c in parent.Controls)
{
if (!c.Visible)
{
continue;
}
c.Location = nextControlLocation;
c.Width = parentDisplayRectangle.Width;
nextControlLocation.Offset(0, c.Height + parent.Space);
}
return false;
}
}
}
Above code works fine, except one thing:
when I'm adding controls to my container they are added correctly (new below parent, 100% width), but when height of controls is bigger than my container height I get horizontal scrollbars, but after adding couple controls more scrollbar is removed.
Same thing happens when I want to resize my container:
How this can be fixed? I just need to remove that horizontal scrollbar.
Of course all improvements are welcome :)
I don't want to use table layout or flow layout as this one gives me exactly when I need.
I need a simple container that orders all child controls from top to bottom and stretches them horizontally so they take as much width so container horizontal scrollbar isn't needed.

Here is a working example that unfortunately, does not use your Layout Engine class. It simply relies on the OnControlAdded and OnControlRemoved methods, and anchoring and setting the AutoScrollMinSize property to specifically make sure the horizontal scrollbar never appears:
internal class VerticalPanel : Panel {
private int space = 10;
public int Space {
get { return space; }
set {
space = value;
LayoutControls();
}
}
protected override void OnControlAdded(ControlEventArgs e) {
base.OnControlAdded(e);
LayoutControls();
}
protected override void OnControlRemoved(ControlEventArgs e) {
base.OnControlRemoved(e);
LayoutControls();
}
private void LayoutControls() {
int height = space;
foreach (Control c in base.Controls) {
height += c.Height + space;
}
base.AutoScrollMinSize = new Size(0, height);
int top = base.AutoScrollPosition.Y + space;
int width = base.ClientSize.Width - (space * 2);
foreach (Control c in base.Controls) {
c.SetBounds(space, top, width, c.Height);
c.Anchor = AnchorStyles.Left | AnchorStyles.Top | AnchorStyles.Right;
top += c.Height + space;
}
}
}

You can set the AnchorProperty at you Buttons like:
button1.Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Top;
So they'll be resized horizontally

Related

Groupbox created in code doesn't dock correctly to parent TableLayoutPanel

In my C# WinForm application I have a tableLayoutPanel with only one row and two columns. The left column contains a TreeView, the right one another tableLayoutPanel which was set to Dock: Fill in the designer. I want to create several GroupBoxes programmatically that should use the full width of this second column also when the form is resized.
In the designer of Visual Studio it looks like this:
But when the application runs, the GroupBoxes don't use the complete width and it looks like this:
I'm using two methods to create the GroupBoxes (you can ignore the int values):
private void addDashboardRow(int i, int s)
{
// Add one row to dashboardTableLayoutPanel
sensorTableLayoutPanel.RowCount++;
// Maybe this one is wrong, but SizeType.AutoSize doesn't work either
sensorTableLayoutPanel.RowStyles.Add(new RowStyle(SizeType.Absolute, 75f));
sensorTableLayoutPanel.Controls.Add(createSensorGroupbox(i, s));
}
private GroupBox createSensorGroupbox(int i, int sen)
{
GroupBox g = new GroupBox();
//g.Width = 500; // I don't want fixed sizes, height of 75px is sufficient
//g.Height = 75;
g.Dock = DockStyle.Fill;
g.Text = "Sensor " + sen.ToString();
// just one label as example...
Label temp = new Label();
temp.Text = "Temperature: ";
temp.Location = new Point(5, 20);
Label tempVal = new Label();
tempVal.Text = "°C";
tempVal.Location = new Point(5, 50);
tempVal.Name = "Sensor" + sen.ToString() + "_temp";
//[... more labels]
g.Controls.Add(temp);
g.Controls.Add(tempVal);
return g;
}
My questions are:
How can I fit the GroupBox into the complete width of the parent container?
How can I delete all rows? E.g. when looking up the sensors during run time all GroupBoxes are dublicated, but everything should be created newly.
Layout of the question: how should I format terms like TableLayoutPanel correctly in this forum? It's my second question here and I haven't found out, yet.
For the second question. Currently I'm using this method that doesn't do what I'm expecting:
private void clearDashboardTable()
{
if (sensorTableLayoutPanel.Controls.Count == sensorList.Count) {
//MessageBox.Show(sensorTableLayoutPanel.Controls.Count.ToString());
foreach (Control con in sensorTableLayoutPanel.Controls)
{
sensorTableLayoutPanel.Controls.Remove(con);
con.Dispose();
}
//sensorTableLayoutPanel.Dispose();
sensorList.Clear();
}
}
I commented out what I've tried so far in the above code or added additional comments. I used this topic to try to understand the basics of dynamic groupbox creation: Add Row Dynamically in TableLayoutPanel
One way to achieve the outcome you describe is to make a custom UserControl containing a docked GroupBox that contains a docked TableLayoutPanel.
To adjust the width when the container changes, attach to the SizeChanged event of the parent.
public partial class CustomGroupBox : UserControl
{
public CustomGroupBox() => InitializeComponent();
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
if((Parent != null) &&!DesignMode)
{
Parent.SizeChanged += onParentSizeChanged;
}
}
private void onParentSizeChanged(object? sender, EventArgs e)
{
if(sender is FlowLayoutPanel flowLayoutPanel)
{
Debug.WriteLine($"{flowLayoutPanel.Width}");
Width =
flowLayoutPanel.Width -
SystemInformation.VerticalScrollBarWidth;
}
}
public new string Text
{
get=>groupBox.Text;
set=>groupBox.Text = value;
}
}
Main Form
The main form is laid out similar to your image except to substitute a FlowLayoutPanel on the right side.
Now test it out with this minimal code in the method that loads the Main Form:
public partial class MainForm : Form
{
int _id = 0;
public MainForm()
{
InitializeComponent();
flowLayoutPanel.AutoScroll= true;
for (int i = 0; i < 5; i++)
{
var customGroupBox = new CustomGroupBox
{
Text = $"Sensor {++_id}",
Width = flowLayoutPanel.Width - SystemInformation.VerticalScrollBarWidth,
Padding = new Padding(),
Margin = new Padding(),
};
flowLayoutPanel.Controls.Add(customGroupBox);
}
}
}
Clear
An example of clearing the sensors would be to add a button and set a handler in the same Load method:
buttonClear.Click += (sender, e) => flowLayoutPanel.Controls.Clear();

Dynamically created PictureBox rendering problem

The end goal is a somewhat playable memory game. Currently, I'm stuck on a rendering problem. I have the following classes:
Field, which is an abstract UserControl:
public abstract class Field : UserControl
{
protected PictureBox _pictureBox;
public Field()
{
_pictureBox = new PictureBox();
_pictureBox.Image = Properties.Resources.empty;
_pictureBox.SizeMode = PictureBoxSizeMode.StretchImage;
_pictureBox.BorderStyle = BorderStyle.FixedSingle;
this.Controls.Add(_pictureBox);
}
// ...
// some abstract methods, not currently important
}
MemoryField, which derives from Field:
public class MemoryField : Field
{
public MemoryField(Form parent, int xPos, int yPos, int xSize, int ySize)
{
_pictureBox.ClientSize = new Size(xSize, ySize);
_pictureBox.Location = new Point(xPos, yPos);
_pictureBox.Parent = parent;
}
// ...
}
And finally, MainForm which is an entry point for my application:
public partial class MainForm : Form
{
private readonly int fieldWidth = 100; // 150 no rendering problems at all
private readonly int fieldHeight = 100;
public MainForm() { InitializeComponent(); }
private void MainForm_Load(object sender, EventArgs e)
{
for (int y = 0; y < 6; y++) // 6 rows
{
for (int x = 0; x < 10; x++) // 10 columns
{
Field field = new MemoryField(this,
x * (fieldWidth + 3), // xPos, 3 is for a small space between fields
labelTimer.Location.Y + labelTimer.Height + y * (fieldHeight + 3), // yPos
fieldWidth,
fieldHeight);
this.Controls.Add(field);
}
}
}
}
Here's where my problem lies:
In those for loops I'm trying to generate a 6x10 grid of Fields (with each containing a PictureBox 100x100 px in size). I do that almost successfully, as my second field is not rendered correctly:
Only thing I found that works (fixes the problem completely) is making field bigger (i.e. 150px). On the other hand, making it smaller (i.e. 50px) makes the problem even bigger:
Maybe useful information and things I've tried:
My MainForm is AutoSize = true; with AutoSizeMode = GrowAndShrink;
My MainForm (initially) doesn't contain any components except menuStrip and label
I tried changing PictureBox.Image property, that didn't work.
I tried creating the grid with just PictureBox controls (not using Field as a PictureBox wrapper), that did work.
I tried placing labelTimer in that "problematic area" which does fix the problem depending on where exactly I put it. (because field positioning depends on labelTimer 's position and height)
I tried relaunching visual studio 2017, didn't work.
Of course, I could just change the size to 150px and move on, but I'm really curious to see what's the root of this problem. Thanks!
The easiest thing to do to fix the problem is something you've already tried - using directly a PictureBox instead of a Field. Now, considering that you only use Field to wrap a PictureBox, you could inherit from PictureBox instead of just wrapping it.
Changing your classes to these will fix the issue as you've noticed:
public abstract class Field : PictureBox {
public Field() {
Image = Image.FromFile(#"Bomb01.jpg");
SizeMode = PictureBoxSizeMode.StretchImage;
BorderStyle = BorderStyle.FixedSingle;
Size = new Size(100, 100);
}
// ...
// some abstract methods, not currently important
}
public class MemoryField : Field {
public MemoryField(Form parent, int xPos, int yPos, int xSize, int ySize) {
ClientSize = new Size(xSize, ySize);
Location = new Point(xPos, yPos);
}
// ...
}
The real reason it was not working has to do with both sizing and positioning of each Field and their subcomponents. You should not set the Location of each _pictureBox relatively to its parent MemoryField, but rather change the Location of the MemoryField relatively to its parent Form.
You should also set the size of your MemoryField to the size of its child _pictureBox otherwise it won't size correctly to fit its content.
public class MemoryField : Field {
public MemoryField(Form parent, int xSize, int ySize) {
_pictureBox.ClientSize = new Size(xSize, ySize);
// I removed the setting of Location for the _pictureBox.
this.Size = _pictureBox.ClientSize; // size container to its wrapped PictureBox
this.Parent = parent; // not needed
}
// ...
}
and change your creation inner loop to
for (int x = 0; x < 10; x++) // 10 columns
{
Field field = new MemoryField(this,
fieldWidth,
fieldHeight);
field.Location = new Point(x * (fieldWidth + 3), 0 + 0 + y * (fieldHeight + 3)); // Set the Location here instead!
this.Controls.Add(field);
}

Problems with custom canvas

I tried to write my own custom Canvas and wanted to draw a little labyrinth which consist of little rectangles. My Problem is, that I just get 4 little points on my screen and not 4 Rectangles (when trying it with a 2 X 2 field).
Here is some Code:
public class LabyrinthCanvas : System.Windows.Controls.Canvas
{
public static readonly int RectRadius = 60;
public ObservableCollection<ObservableCollection<Rect>> Rectangles;
public LabyrinthCanvas()
{
Rectangles = new ObservableCollection<ObservableCollection<Rect>>();
}
public void AddRectangles(int Height, int Width)
{
for (int iHeight = 0; iHeight < Height; iHeight++)
{
ObservableCollection<Rect> newRects = new ObservableCollection<Rect>();
newRects.CollectionChanged += RectanglesChanged;
Rectangles.Add(newRects);
for (int iWidth = 0; iWidth < Width; iWidth++)
{
Rect rect = new Rect(iHeight * RectRadius, iWidth * RectRadius);
Rectangles[iHeight].Add(rect);
}
}
}
public void RectanglesChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
{
foreach (object rect in e.NewItems)
{
if (rect is Rect)
{
this.Children.Add(((Rect)rect).innerRectangle);
System.Windows.Controls.Canvas.SetTop(((Rect)rect).innerRectangle, ((Rect)rect).YPos);
System.Windows.Controls.Canvas.SetLeft(((Rect)rect).innerRectangle, ((Rect)rect).XPos);
}
}
}
else if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove)
{
foreach (Rect rect in e.OldItems)
{
this.Children.Remove(rect.innerRectangle);
}
}
}
}
public class Rect : INotifyPropertyChanged
{
public Rect(int YPos, int XPos)
{
innerRectangle.Stroke = System.Windows.Media.Brushes.Black;
innerRectangle.Fill = System.Windows.Media.Brushes.Blue;
this.YPos = YPos;
this.XPos = XPos;
}
public System.Windows.Shapes.Rectangle innerRectangle = new System.Windows.Shapes.Rectangle();
public int YPos;
public int XPos;
}
I think the important thing is that:
this.Children.Add(((Rect)rect).innerRectangle);
System.Windows.Controls.Canvas.SetTop(((Rect)rect).innerRectangle, ((Rect)rect).YPos);
System.Windows.Controls.Canvas.SetLeft(((Rect)rect).innerRectangle, ((Rect)rect).XPos);
Im using a own Class "Rect" because i need some extra properties which i removed from the shown code and I cant inherit from Rectangle.
I'm not entirely sure what you want your end result to look like, so I probably won't be able to suggest the exact solution you're after.
That said, the reason you're obtaining small points on your screen, rather than rectangles, is because the canvas is rendering the innerRectangle of your Rect object, at the specified coordinates, but you're never initialising setting the dimensions of that innerRectangle.
The dots you're seeing are those width/heightless rectangles, which are having the Black stroke rendered (the dot).
You can see what's going on if you try something along these lines:
public System.Windows.Shapes.Rectangle innerRectangle = new System.Windows.Shapes.Rectangle() { Width = 10, Height = 10 };

Specifying number of items per row using a WrapPanel

I'm attempting to create an application that looks much like the Windows 8 Metro UI with the tiles.. right now if you click on a tile I have an animation that increases the width and reveals more info on that tile.
I have the tiles laid out in a WrapPanel which is nice because if I resize the tile the other tiles next to it move and keep it's margin perfectly. However I've been researching this for awhile, I would like to if possible limit the number of items in the wrap panel to two wide (Right now it's three wide) as if you select one of the tiles it resizes itself and pushes a tile next to it (or if it's the end tile it will push itself) to the next row, while my animations are smooth it doesn't look the best presentation-wise..
Could someone point me towards how I might specify my wrappanel to only have a width of two items across?
Any help is very much appreciated.
Try making your own wrap panel deriving from standard wrap panel as described in this post in detail. Post addresses the same issue which you are trying to solve.
public class MyWrapPanel : WrapPanel
{
public int MaxRows
{
get { return (int)GetValue(MaxRowsProperty); }
set { SetValue(MaxRowsProperty, value); }
}
public static readonly DependencyProperty MaxRowsProperty =
DependencyProperty.Register("MaxRows", typeof(int), typeof(MyWrapPanel), new UIPropertyMetadata(4));
protected override Size ArrangeOverride(Size finalSize)
{
Point currentPosition = new Point();
double ItemMaxHeight = 0.0;
int RowIndex = 0;
foreach (UIElement child in Children)
{
ItemMaxHeight = ItemMaxHeight > child.DesiredSize.Height ? ItemMaxHeight : child.DesiredSize.Height;
if (currentPosition.X + child.DesiredSize.Width > this.DesiredSize.Width)
{
currentPosition = new Point(0, currentPosition.Y + ItemMaxHeight);
ItemMaxHeight = 0.0;
RowIndex++;
}
if (RowIndex < MaxRows)
{
child.Visibility = System.Windows.Visibility.Visible;
Rect childRect = new Rect(currentPosition, child.DesiredSize);
child.Arrange(childRect);
}
else
{
Rect childRect = new Rect(currentPosition, new Size(0,0));
child.Arrange(childRect);
}
currentPosition.Offset(child.DesiredSize.Width, 0);
}
return finalSize;
}
protected override Size MeasureOverride(Size availableSize)
{
return base.MeasureOverride(availableSize);
}
}

Windows Forms Separator Control

Where in VS2010 can I find a horizontal separator control, as can be found in Outlook settings (screenshots below)?
https://jira.atlassian.com/secure/attachment/14933/outlook+settings.jpg
http://www.keithfimreite.com/Images/OutlookSettings3.gif
Note: VB.NET preferred, but C# answers okay.
If I'm not mistaken, that's just a Line control, but I don't think that control exists anymore. Here is a workaround.
label1.AutoSize = False
label1.Height = 2
label1.BorderStyle = BorderStyle.Fixed3D
Even though this has been answered, I found the following to be what I need based partly on smoore's answer.
Create a new control. Edit the code to be the following:
public partial class Line : Label
{
public override bool AutoSize
{
get
{
return false;
}
}
public override Size MaximumSize
{
get
{
return new Size(int.MaxValue, 2);
}
}
public override Size MinimumSize
{
get
{
return new Size(1, 2);
}
}
public override string Text
{
get
{
return "";
}
}
public Line()
{
InitializeComponent();
this.AutoSize = false;
this.Height = 2;
this.BorderStyle = BorderStyle.Fixed3D;
}
}
Replace Line with the control's class name you want. This will put a separator that will allow you to resize in the designer and disables adding text, changing the autosize forces the size's height to be 2 and width to be whatever you want, and disables adding text.
It's not actually included in the standard set of controls (pretty sure it used to be back in the day!) but you can easily create your own or cheat by using a GroupBox with no text and a height of 1px.
UserControl to provide the same thing: (Not written by me, source: http://social.msdn.microsoft.com/Forums/en-US/winforms/thread/0d4b986e-3ed0-4933-a15d-4b42e02005a7/)
public partial class LineSeparator:UserControl
{
public LineSeparator()
{
InitializeComponent();
this.Paint += new PaintEventHandler(LineSeparator_Paint);
this.MaximumSize = new Size(2000, 2);
this.MinimumSize = new Size(0, 2);
this.Width = 350;
}
private void LineSeparator_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
g.DrawLine(Pens.DarkGray, new Point(0, 0), new Point(this.Width, 0));
g.DrawLine(Pens.White, new Point(0, 1), new Point(this.Width, 1));
}
}
I wrote a custom control just for this purpose. It supports both vertical and horizontal modes. Just install my [small] control suite and drag the separator control onto the form and place it anywhere you want.
Install-Package ALMSTWKND -Version 1.0.0
The controls will show up in the Toolbox pane after installation.

Categories

Resources