I've been using a UserControl to display a list of controls. I initially had AutoScroll enabled, and then opted to not using it. I chose against using it as it stood out and simply didn't 'look' good with the controls theme I've been using.
I took a shot at a framework called MetroFramework, and I've opted to use the MetroScrollBar scrollbar control for a vertical scrollbar.
I've fully disabled AutoScroll, and I then decided to implement the Scrollbar. I simply did this by:
scbMain.Scroll += (sender, e) => { VerticalScroll.Value = scbMain.Value; };
(where scbMain is the Scrollbar I'm discussing)
This works, but not as expected. As soon as I scroll, I get a crazy flickering effect from the default scrollbar, as shown here. A longer list has the same effect, but more pronounced.
I've attempted to hide the existing scrollbars:
VerticalScroll.Visible = false;
HorizontalScroll.Visible = false;
VerticalScroll.Enabled = false;
HorizontalScroll.Enabled = false;
This has had no effect on fixing my issue.
It should be noted: My scrollbar is docked to the right and there're no other container controls within the UserControl.
Ok. Problem solved. The issue was in this line of code:
scbMain.Scroll += (sender, e) => { ----> /*(Here*/ VerticalScroll.Value = scbMain.Value; <---- };
You are actully setting the scroll value of your user control, basically you tell the system to invoke the autoscroll property to set the value!
The correct way is to NOT autoscroll the user control but to scroll a container inside eg a panel. So add a panel to your user control. You are going to scroll the panel and all the controls inside it (in this example i will add the button).
this.btnExample.Location = new System.Drawing.Point(62, 0);
this.btnExample.Name = "btnExample";
this.btnExample.Size = new System.Drawing.Size(75, 390);
this.btnExample.TabIndex = 1;
this.btnExample.Text = "Out of Bounds";
this.btnExample.UseVisualStyleBackColor = true;
this.panel1.Controls.Add(this.btnExample);
this.panel1.Location = new System.Drawing.Point(0, 0);
this.panel1.Name = "panel1";
this.panel1.Size = new System.Drawing.Size(270, 391); //the width must fit inside your user control and the height was arbitrary
this.panel1.TabIndex = 2;
this.Controls.Add(this.panel1);
this.Controls.Add(this.scbMain);
this.Name = "CtrlScroll";
this.Size = new System.Drawing.Size(474, 300);
The scrolling:
public CtrlScroll() {
InitializeComponent();
scbMain.Scroll += ( sender, e ) => {
//Normally the if statement whouldn't be needed but the metro srollbar
//has a weird behaviour when the scroll value becomes max
if( scbMain.Value > panel1.Height - this.Height ) {
panel1.Top = -( panel1.Height - this.Height );
}
else {
panel1.Top = -scbMain.Value;
};
};
int maxVertical = panel1.Height;
// SmallChange is typically 1%.
int smallChangeVertical = Math.Max( (int)( maxVertical / 100 ), 1 );
// LargeChange is one page.
int largeChangeVertical = this.Height;
scbMain.Minimum = 0;
scbMain.Maximum = maxVertical;
scbMain.SmallChange = smallChangeVertical;
scbMain.LargeChange = largeChangeVertical;
}
Related
The problem
I'm dynamically adding Buttons to the WinForm. As I do so, I'm repositioning existing Buttons to prevent overlap. The AutoSize property is being used to automatically set Width.
For longer text (that pushes Buttons beyond their default Width), the below code doesn't work.
For example:
b.Width is 75 before AutoSize is set
b.Width is 75 after AutoSize is set
When shifting other Buttons, it shifts them by b.Width + buffer = 83
However after addButton()completes, the AutoSize kicks in and sets the width to 150, overlapping the next Button which is only 83 pixels away instead of 158.
AutoSize appears to change the size of the control too late for it to be of use. How can I make it happen immediately?
Attempt 1 - Code
public void addButton(string text)
{
const int buffer = 8;
//Construct new button
Button b = new Button();
b.Text = text;
b.AutoSize = true;
b.Location = new Point(0, 0);
//Shift over all other buttons to prevent overlap
//b.Width is incorrect below, because b.AutoSize hasn't taken effect
for (int i = 0; i < Controls.Count; i++)
if (Controls[i] is Button)
Controls[i].Location = new Point(Controls[i].Location.X + b.Width + buffer, Controls[i].Location.Y);
Controls.add(b);
}
Attempt 2
Searched Google and StackOverflow for the following:
c# autosize immediately
c# autosize fast
c# autosize not working
Attempt 3
Asking here.
Last Resort
If nothing else works, a timer could be set to reposition Buttons on each tick. However this is very sloppy design, and doesn't aid in learning the intricacies of AutoSize. I'd like to avoid this workaround if possible.
The AutoSize and AutoSizeMode mode are applied only when the control is parented to the another control or form.
So invoke first
Controls.Add(b);
Now the b.Size will the adjusted accordingly and can be used in the calculations.
Alternatively, instead of Size property you can use the GetPreferredSize method to get the correct size without actually applying AutoSize and use it inside the calculations:
var bSize = b.GetPreferredSize(Size.Empty);
//Shift over all other buttons to prevent overlap
//b.Width is incorrect below, because b.AutoSize hasn't taken effect
for (int i = 0; i < Controls.Count; i++)
if (Controls[i] is Button)
Controls[i].Location = new Point(Controls[i].Location.X + bSize.Width + buffer, Controls[i].Location.Y);
The FlowLayoutPanel control does this work for you.
Place one on your form and try adding buttons in the following manner:
Button b = new Button();
b.AutoSize = true;
b.Text = text;
flowLayoutPanel1.SuspendLayout();
flowLayoutPanel1.Controls.Add(b);
flowLayoutPanel1.Controls.SetChildIndex(b, 0);
flowLayoutPanel1.ResumeLayout();
You can subscribe to the Resize event of the last button added. This will allow you to accurately change the locations of all of the buttons because now all of the buttons have been AutoSized.
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
var button1 = NewButton(0);
button1.Location = new Point(10, 10);
var button2 = NewButton(1);
button2.Location = new Point(button1.Right, 10);
var button3 = NewButton(2);
button3.Location = new Point(button2.Right, 10);
button3.Resize += (s, e) =>
{
button2.Location = new Point(button1.Right, 10);
button3.Location = new Point(button2.Right, 10);
};
Controls.Add(button1);
Controls.Add(button2);
Controls.Add(button3);
}
private Button NewButton(int index)
{
return new Button()
{
Text = "ButtonButtonButton" + index.ToString(),
AutoSize = true
};
}
}
In my application I've got the following situation:
I've got a Windows Form with a Tab Control with several tabs. Each tab contains arbitrary content which is added by other classes upon startup or during runtime.
I want to set up the tabs in a way that scrollbars appear automatically as soon as the Form is too small for the tab's panel to display everything.
What I've tried so far is setting the tab page's AutoScroll = true and setting the AutoScrollMinSize property to the size of the panel.
This did not work as expected as the panel's Size always seems to be (200, 100) independent of its contents.
I've created a small example application (code below) which demonstrates the issue. If you resize the form, you'll see that scroll bars only appear if the Form gets smaller than the panel (default size of (200, 100)) rather than the text box in the panel (size of 300, 150). If you set AutoScrollMinSize manually (uncomment line 34), it behaves as expected.
The question is: How can the tab page retrieve the actual size of what is displayed in it?
I could probably recurse through all controls and try calculating the size myself - but this feels really bad.
PS: Please do not suggest setting the size of the panel to the size of the label, as the actual panels are much more complex than that. ;-)
Code:
Simply create an Application in Visual Studio and override Program.cs with the following code:
using System;
using System.Windows.Forms;
namespace ScrollbarTest
{
static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
var sampleForm = CreateSampleForm();
Application.Run(sampleForm);
}
private static Form CreateSampleForm()
{
var sampleForm = new Form() { };
var tabControl = new TabControl() { Dock = DockStyle.Fill };
var tabPage = new TabPage("Test") { AutoScroll = true };
sampleForm.Controls.Add(tabControl);
tabControl.TabPages.Add(tabPage);
var samplePanel = CreateSamplePanel();
tabPage.Controls.Add(samplePanel);
// this does not provide the right size
tabPage.AutoScrollMinSize = samplePanel.Size;
// uncomment this to make it work
//tabPage.AutoScrollMinSize = new System.Drawing.Size(300, 150);
return sampleForm;
}
private static Control CreateSamplePanel()
{
// As an example, create a panel with a text box with a fixed size.
var samplePanel = new Panel() { Dock = DockStyle.Fill };
var sampleSize = new System.Drawing.Size(300, 150);
var textBox = new TextBox()
{
Dock = DockStyle.Fill,
MinimumSize = sampleSize,
MaximumSize = sampleSize,
Size = sampleSize
};
samplePanel.Controls.Add(textBox);
return samplePanel;
}
}
}
The samplePanel.Size returns (200,100). In your CreateSamplePanel method, if you set samplePanel.MinimumSize = sampleSize; then your code will work.
Panels don't calculate their size properties (e.g. Size, MinimumSize, PreferredSize) based on their child controls. You will have to subclass Panel and provide that behavior. Even TableLayoutPanel and FlowLayoutPanel don't correctly calculate the PreferredSize property, which is surprising. At the very least, normally you override the GetPreferredSize(Size proposedSize) method, and optionally have the MinimumSize property return the PreferredSize property.
It's worth noting that DockStyle.Fill and MinimumSize are at odds with each other. TabPage controls are inherently DockStyle.Fill mode, which is why you have to set the AutoScrollMinSize property.
Edit: Isn't there any existing function which retrieves the total required size of a list of controls (recursively), e.g. through their X/Y and Size?
It's up to the host container itself (e.g. TableLayoutPanel) to calculate its PreferredSize correctly because only it knows the exact details of how its layout is performed.
You can set the AutoSize property to true and then hope that GetPreferredSize(...)/PreferredSize calculates the right size. For TableLayoutPanel, I recall there was a case where it wasn't calculating correctly and I had to subclass it and override the GetPreferredSize(...) method. GetPreferredSize(...) won't be called unless AutoSize is true.
If you're talking about a plain Panel or UserControl, by default these use the WYSIWYG LayoutEngine, and do not calculate the PreferredSize. You could subclass and then calculate maximum control.X + control.Width and same thing for height, and use that as the preferred size.
First try setting AutoSize to true and see if that works for you. If not, you might have to override the GetPreferredSize(...) method. Here is a crude example:
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
var sampleForm = new Form() { AutoScroll = true };
var panel = new MyPanel() { AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink, BackColor = Color.LightYellow };
for (int i = 0; i < 6; i++) {
for (int j = 0; j < 3; j++) {
Button b = new Button { Text = "Button" + panel.Controls.Count, AutoSize = true };
b.Click += delegate {
MessageBox.Show("Preferred Size: " + panel.PreferredSize);
};
panel.Controls.Add(b, j, i);
}
}
sampleForm.Controls.Add(panel);
Application.Run(sampleForm);
}
private class MyPanel : TableLayoutPanel {
public override Size MinimumSize {
get {
return PreferredSize;
}
set {
}
}
public override Size GetPreferredSize(Size proposedSize) {
Size s = new Size();
int[] harr = new int[100];//this.RowCount];
int[] warr = new int[100];//this.ColumnCount];
foreach (Control c in this.Controls) {
var cell = this.GetPositionFromControl(c);
var ps = c.PreferredSize;
Padding m = c.Margin;
int w = ps.Width + m.Horizontal;
int h = ps.Height + m.Vertical;
if (w > warr[cell.Column])
warr[cell.Column] = w;
if (h > harr[cell.Row])
harr[cell.Row] = h;
}
foreach (int w in warr)
s.Width += w;
foreach (int h in harr)
s.Height += h;
return s;
}
}
I try to create simple app with 2 columns using SpliterContainer and control panel with buttons. And I would like that on every screen it will look good. That's why I decided to use relative position of elements.
I read documentation and different forums, but I get something strange. Second column of splitter doesn't appear at all.
Please, can you help me find the reason of that problem?
private void Form1_Load(object sender, EventArgs e)
{
WindowState = FormWindowState.Maximized;
int screenWidth = Screen.PrimaryScreen.Bounds.Width;
int screenHeight = Screen.PrimaryScreen.Bounds.Height;
//set form size
this.Size = new Size(screenWidth, screenHeight);
//set button panel size
const double percentOfHeightPanel = 0.05;
int heightOfPanelButton = Convert.ToInt32(screenHeight * percentOfHeightPanel);
this.panel_button.Size = new System.Drawing.Size(screenWidth, heightOfPanelButton);
this.panel_button.Location = new Point(0, 0);
//set splitContainer size
int widthOfContainer = Convert.ToInt32(0.5 * screenWidth);
int heightOfContainers = Convert.ToInt32(screenHeight * (0.95));
splitContainer1.Panel1.MinimumSize = new Size(widthOfContainer, heightOfContainers);
splitContainer1.Panel2.MinimumSize = new Size(widthOfContainer, heightOfContainers);
splitContainer1.Location = new Point(0, heightOfPanelButton);
//this.splitContainer1.Panel2MinSize = screenWidth - widthOfContainer;
//set textBox size
this.textBox1.Multiline = true;
this.textBox1.Location = new Point(0, heightOfPanelButton);
this.textBox1.MinimumSize = new System.Drawing.Size(widthOfContainer, heightOfContainers);
this.textBox2.Multiline = true;
this.textBox2.Location = new Point(widthOfContainer, heightOfPanelButton);
this.textBox1.MinimumSize = new System.Drawing.Size(widthOfContainer, heightOfContainers);
}
If you want two have two splitter panels of the same size set
splitContainer1.SplitterDistance =
(splitContainer1.Width - splitContainer1.SplitterWidth) / 2;
Then set
splitContainer1.IsSplitterFixed = true;
You can set these two properties manually at design time. The user will then not be able to resize the panels and the panels will automatically resize to be of same size.
Consider using a TableLayoutPanel instead.
If further, the two sides should look the same, place your controls on a UserControl and place two instances of them into the two panels with a docked property set to Fill.
I'm creating a GUI in C# using WinForms.
I'm trying to position programaticaly created panels one below the other. As the content of these panel can vary depending on their content, I'm using Panel.AutoSize to let WinForms perform the correct resizing.
The problem is: if I'm using Panel.Height (or Panel.Size.Height) right after populating the Panel, the value returned is always my default value. The resizing do occur, as I can see when launching the app, but I just don't know when.
Here's a simplified version of what I'm doing:
this.SuspendLayout();
int yPos = 0;
foreach (String entry in entries)
{
Panel panel = new Panel();
panel.SuspendLayout();
panel.AutoSize = true;
panel.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowOnly;
panel.BackColor = System.Drawing.SystemColors.Window; // Allows to see that the panel is resized for dispay
panel.Location = new System.Drawing.Point(0, yPos);
panel.Size = new System.Drawing.Size(this.Width, 0);
this.Controls.Add(panel);
Label label = new Label();
label.AutoSize = true;
label.Location = new System.Drawing.Point(0, 0);
label.MaximumSize = new System.Drawing.Size(panel.Width, 0);
label.Text = entry;
panel.Controls.Add(label);
panel.ResumeLayout(false);
panel.PerformLayout();
yPos += panel.Height; // When breaking here, panel.Height is worth 0
yPos += label.Height; // This works perfectly, label.Height was updated according to the text content when breaking at that point
}
this.ResumeLayout(false);
this.PerformLayout();
So the real question is: How can I get the updated Panel.Size after adding controls to it, to get its proper height value?
Note: I know I can use the TextBox height, but I find it inelegant and impractical, as in my actual code there are more controls in the Panel and I need to use that panel height a few lines below.
What I beleive is happening is that the Size of the Panel will be determined when you do PerformLayout on its Parent. You can make it work like you are wanting by moving the panel's parent SuspendLayout / ResumeLayout code into the Loop.
int yPos = 0;
foreach (String entry in entries)
{
this.SuspendLayout();
Panel panel = new Panel();
panel.SuspendLayout();
panel.AutoSize = true;
panel.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowOnly;
panel.BackColor = System.Drawing.SystemColors.Window; // Allows to see that the panel is resized for dispay
panel.Location = new System.Drawing.Point(0, yPos);
panel.Size = new System.Drawing.Size(this.Width, 0);
this.Controls.Add(panel);
Label label = new Label();
label.AutoSize = true;
label.Location = new System.Drawing.Point(0, 0);
label.MaximumSize = new System.Drawing.Size(panel.Width, 0);
label.Text = entry;
panel.Controls.Add(label);
panel.ResumeLayout(true);
this.ResumeLayout(true);
yPos += panel.Height; // When breaking here, panel.Height is worth 0
//yPos += label.Height; // This works perfectly, label.Height was updated according to the text content when breaking at that point
}
this.PerformLayout();
I'm trying to simulate an Android UI element that unfortunately doesn't exist in Windows 7 phone: ListPreference
I thought about using a Popup, that would take exactly the whole screen (to simulate a modal window).
So the popup would be made of the following elements:
Popup -> Canvas -> Border -> StackPanel -> RadioButtons
The Canvas would be fully transparent (or lightly whitish to clearly show that the element underneath aren't available)
The border would be made so it only big enough to contain all the RadioButtons
Then the StackPanel would be opaque and black.
Unfortunately, if I make the bottom canvas transparent, all children elements are also transparent. I can only make the elements more transparent.
The way transparency works is slightly different than with Android or iPhone (where it's quite easy to have a parent fully transparent, but opaque children).
Is there a way to make a parent fully transparent with the children opaque?
Or maybe someone could suggest another way to simulate a modal window.
Who knows, maybe someone even developed a ListPreference-like UIElement :)
Thank you
Here is how I ended up doing it.
It works in a similar fashion as ListPreference on Android. The constructor takes a string, an array of string and an int indicating which is the default value
When the windows is closed, the delegate Dismissed is called..
So you call it like so:
string[] choices = { "Choice 1", "Choice 2", "Choice3" };
ListPreference lp = new ListPreference("name", choices, 1);
lp.dismissed += new ListPreferences.DismissedHandler(lp_Dismissed);
the code:
public class ListPreference
{
Popup p;
string Name;
int oldValue;
public delegate void DismissedHandler(string name, bool changed, int newvalue);
public event DismissedHandler Dismissed;
public bool IsOpen
{
get
{
return p.IsOpen;
}
set
{
p.IsOpen = value;
}
}
public ListPreference(string name, Array elements, int default_value)
{
p = new Popup();
Name = name;
Dismissed = null;
oldValue = default_value;
double height = (App.Current.RootVisual as FrameworkElement).ActualHeight;
double width = (App.Current.RootVisual as FrameworkElement).ActualWidth;
p.VerticalOffset = SystemTray.IsVisible ? 32.0 : 0.0;
p.Height = height;
p.Width = width;
Canvas canvas = new Canvas();
SolidColorBrush colorBrush = new SolidColorBrush(Colors.Black);
colorBrush.Opacity = 0.75;
//Color.FromArgb(0xff, 0x8a, 0x8a, 0x8a));
canvas.Background = colorBrush;
//canvas.Opacity = 0.765;
canvas.Height = height;
canvas.Width = width;
p.Child = canvas;
Border border = new Border();
border.Width = width - 50.0 * 2.0;
border.BorderBrush = new SolidColorBrush(Colors.LightGray);
border.BorderThickness = new Thickness(5.0);
border.Background = new SolidColorBrush(Colors.Black);
canvas.Children.Add(border);
StackPanel panel2 = new StackPanel();
panel2.Orientation = System.Windows.Controls.Orientation.Vertical;
int i = 0;
foreach (string val in elements)
{
RadioButton radio1 = new RadioButton();
radio1.GroupName = "group1";
radio1.Content = val;
if (i == default_value)
radio1.IsChecked = true;
int j = i;
radio1.Click += (sender, args) => radio1_Checked(radio1, j);
i++;
panel2.Children.Add(radio1);
}
Button button1 = new Button();
button1.Background = new SolidColorBrush(Colors.Black);
button1.Foreground = new SolidColorBrush(Colors.White);
button1.Opacity = 1.0;
button1.Content = "Cancel";
button1.Margin = new Thickness(5.0);
button1.Click += new RoutedEventHandler(closeButton_Click);
panel2.Children.Add(button1);
border.Child = panel2;
// Open the popup.
p.IsOpen = true;
p.UpdateLayout();
border.Height = panel2.DesiredSize.Height + 5.0 * 2.0;
border.SetValue(Canvas.TopProperty, (height - border.Height) / 2.0);
border.SetValue(Canvas.LeftProperty, (width - border.Width) / 2.0);
p.UpdateLayout();
}
void closeButton_Click(object sender, RoutedEventArgs e)
{
// Close the popup.
p.IsOpen = false;
if (Dismissed != null)
{
Dismissed(Name, false, -1);
}
}
void radio1_Checked(object sender, int idx)
{
p.IsOpen = false;
if (Dismissed != null)
{
Dismissed(Name, idx != oldValue, idx);
}
}
}
I would suggest creating a Usercontrol that would do what you need. Set the LayoutRoot grid's background to PhoneSemitransparentBrush or changing the opacity will change the child element's opacity as well. Then your child elements can have any opacity you'd like. You can add this control as a child to the popup. Additionally, you can add doubleanimation to the popup with the opened and closed event triggers. Change the design height of the UserControl to 480x760 to simulate full page.
To answer your question. Using resources like PhoneSemitransparentBrush and TransparentBrush for the Canvas background is one of your options. Opacity will change the opacity of the whole UIElement including its children.