Tuesday, April 27, 2010

Create CheckBox Controls Inside A GroupBox Programmatically

I'm working on a WinForms app to interface with SQL server to grab parameters and other info for various objects. It uses a custom control I discovered on CodeProject to create a collapsible GroupBox control.

Say that I want to create a bunch of new controls based on the output of some SQL function or stored procedure. For example, the system stored procedure sp_databases returns a list of all databases on a given SQL server. 

In my collapsible GroupBox, I might want to create a CheckBox control for each database so that I can let the user choose which database to work on in the current session of the application.

To facilitate this, some pseudocode might be helpful:
  • Instantiate a new CheckBox object.
  • Does my GroupBox contain any other CheckBoxes?
    • NO: Set the CheckBox location so that it appears in the upper-left corner of the GroupBox
    • YES: Grab the X and Y coordinates of the last CheckBox contained in the GroupBox
  • Would the width of an additional CheckBox exceed that of the GroupBox?
    • YES: Set the new CheckBox X coordinate so that it aligns with the first CheckBox in the GroupBox, then bump the Y coordinate down by a set value.
    • NO: Place the new CheckBox to the right of the previous one.
  • Set the Text property of the newly-added CheckBox to a given input string value.

Along these lines, I created the method below, which takes a string as input representing the Text property of the newly-created CheckBox.

       
/// <summary>
        /// Create a new CheckBox control within a collapsible GroupBox, assigning input sName
        /// to the Text property.
        /// </summary>
        /// <param name="sName"></param>
        /// <returns></returns>
        private CheckBox CreateNewCheckBox(string sName)
        {
            int iExistingCheckBoxX = 0;
            int iExistingCheckBoxY = 0;

            int iIncrementX = 100;
            int iIncrementY = 20;

            CheckBox cbNew = new CheckBox();

            cbNew.Width = iIncrementX;

            if (cgbDatabases.Controls.Count == 0)
            {
                cbNew.Location = new Point(cgbDatabases.Location.X + 15, cgbDatabases.Location.Y - 50);
            }
            else
            {
                // Existing checkboxes, so get the Location of the last one.
                iExistingCheckBoxX = cgbDatabases.Controls[cgbDatabases.Controls.Count - 1].Location.X;
                iExistingCheckBoxY = cgbDatabases.Controls[cgbDatabases.Controls.Count - 1].Location.Y;

                // If the new control would overshoot the end of the container, bump it down.
                if ((iExistingCheckBoxX + iIncrementX) + cbNew.Width >= cgbDatabases.Width)
                {
                    iExistingCheckBoxX = cgbDatabases.Location.X + 15;
                    iExistingCheckBoxY = iExistingCheckBoxY + iIncrementY;

                    cbNew.Location = new Point(iExistingCheckBoxX, iExistingCheckBoxY);
                }
                else
                {
                    cbNew.Location = new Point(iExistingCheckBoxX + iIncrementX, iExistingCheckBoxY);
                }
            }

            // Set the Text property according to the input.
            cbNew.Text = sName;

            return cbNew;
        }


In the constructor of my WinForm app, as a test I used the collapsible GroupBox control's Add method in conjunction with the new method to insert a bunch of child CheckBox controls into parent GroupBox. 

        public frmMain()
        {
            InitializeComponent();

            cgbDatabases.Controls.Add(CreateNewCheckBox("CheckBox1"));
            cgbDatabases.Controls.Add(CreateNewCheckBox("CheckBox2"));
            cgbDatabases.Controls.Add(CreateNewCheckBox("CheckBox3"));
            cgbDatabases.Controls.Add(CreateNewCheckBox("CheckBox4"));
            cgbDatabases.Controls.Add(CreateNewCheckBox("Checkbox5"));
            cgbDatabases.Controls.Add(CreateNewCheckBox("Checkbox6"));
            cgbDatabases.Controls.Add(CreateNewCheckBox("Checkbox7"));
            cgbDatabases.Controls.Add(CreateNewCheckBox("Checkbox8"));
            cgbDatabases.Controls.Add(CreateNewCheckBox("Checkbox9"));
            cgbDatabases.Controls.Add(CreateNewCheckBox("Checkbox10"));
            cgbDatabases.Controls.Add(CreateNewCheckBox("Checkbox11"));
        }


The results are as I expect:




Advantages of this method include the ability to tweak the variables within the CreateNewCheckBox method to determine what the starting X and Y coordinates will be based on the Location of the GroupBox; if I move the GroupBox somewhere else in my form's design view, the code won't care, it will simply churn out the CheckBox objects based on the parent control's current position at runtime. Also, it prevents new objects from being created outside the visible width of the GroupBox.

Shortcomings include a lack of checking the height of the GroupBox; if a zillion controls are created within it, there's currently no logic to prevent them from being placed in the nonvisible area of the control. Also, I'm currently not analyzing the incoming string parameter to see whether I want to constrain the resulting object to a uniform height or width, which means a CheckBox with a huge label likely will not display its Text value correctly.

Speaking of dimensions, the method is currently not "smart" enough to roll with the punches in terms of variations in height or width of the resulting controls; it doesn't offer an easy way to make the child objects conform to specific patterns (e.g. the objects appear one after the next from left to right, not up to down or diagonally or some other order). These would need to be tweaked and tested manually to ensure the object labels remain intact, and the objects themselves remain visible and usable. A more elegant solution would be to have the method scrutinize the control to be created and its attributes and compensate.

Baby steps, but nonetheless I found this a helpful exercise in creating CheckBox controls at will within a parent GroupBox, and already am seeing ways in which I can improve this implementation. I wish to thank Jordan Hammond, creator of the CollapsibleGroupBox control from the article I cited above, and also Manoli's code formatter, which was used here to make the C# excerpts presentable.