CBenni
CBenni

Reputation: 565

PropertyGrid control - modify the position of the central splitting vertical line

I have a PropertyGrid control in WinForms (http://msdn.microsoft.com/en-us/library/aa302326.aspx). Now I want to move the middle vertical line more to the left (It is always centered, but my keys are very short, while the values are Paths, which are long. The control places the line in the middle by default, eventhough the user can move it. In respect to user friendlyness, I would like to move the line more to the left programmatically. I have now searched both the WinForms designer properties as well as the members of the PropertyGrid control multiple times and have not found the option (nor any events concerning it).

Is it hidden from sight/modification by being private? Have I simply overseen it? (In that case, I am sincerely sorry) or how can I do this otherwise?

Upvotes: 5

Views: 6595

Answers (3)

B Mallon
B Mallon

Reputation: 11

The solution wasn't completely working for me, although parts of it were.

  • MoveSplitterTo was fine
  • GetInternalLabelWidth wasn't giving back what I was chasing.

This did work for me though:

In the class I was assigning to the PropertyGrid, I created a GetMaxLabelWidth method with a static and instance overload. I could make this more generic, and I will. Later:

    public static int GetMaxLabelWidth(SignDefinition signDefinition, Control targetControl)
    {
        int maxLength = 0;
        foreach (PropertyInfo info in signDefinition.GetType().GetProperties())
        {
            PropertyHandling.GetPropertyDisplayName(signDefinition, info.Name, out string label);
            int length = TextRenderer.MeasureText(label, targetControl.Font).Width;
            if (length > maxLength) maxLength = length;
        }

        return maxLength;
    }

    public int GetMaxLabelWidth(Control targetControl)
    {
        return GetMaxLabelWidth(this, targetControl);
    }

I have a reflection helper in a PropertyHandling namespace for getting the DisplayName attribute if one is assigned, or the property name otherwise:

    public static bool GetPropertyDisplayName(object findInObject, string propName, out string foundDisplayName)
    {
        bool result = false;
        foundDisplayName = string.Empty;
        bool displayNameFound = false;

        PropertyInfo pi = findInObject.GetType().GetProperty(propName);
        IEnumerable<CustomAttributeData> cadc = pi.CustomAttributes;
        foreach (CustomAttributeData cad in cadc)
        {
            if (cad.ToString().Contains("DisplayName"))
            {
                foundDisplayName = cad.ConstructorArguments[0].Value.ToString();
                result = true;
                displayNameFound = true;
            }
        }

        if (!displayNameFound)
        {
            foundDisplayName = propName;
        }

        return result;
    }

I return a bool as I may want to know whether a display name attribute is set or not and handle it differently. In this case, I'm using it lazily.

In the form, at the point I'm assigning the object to the PropertyGrid, I call GetMaxLabelWidth, then use the "MoveSplitterTo" method above. In this case it's on the AfterSelect event of a TreeView, where the object is assigned to the Tag.

    private void signListTree_AfterSelect(object sender, TreeViewEventArgs e)
    {
        TreeNode selNode = ((TreeView)sender).SelectedNode;
        if (selNode.Tag != null)
        {
            signDetailsPropGrid.SelectedObject = selNode.Tag;                
            int labelWidth = ((SignDefinition)selNode.Tag).GetMaxLabelWidth(signDetailsPropGrid);
            signDetailsPropGrid.MoveSplitterTo(labelWidth + 30);
        }
        else
        {
            signDetailsPropGrid.SelectedObject = null;
        }
    }

Adding 30 compensates for the bar on the left of the PropertyGrid.

In order to deal with the form being resized, I add the Resize event:

    private void Form1_Resize(object sender, EventArgs e)
    {
        TreeNode selNode = signListTree.SelectedNode;
        if (selNode.Tag != null)
        {
            int labelWidth = ((SignDefinition)selNode.Tag).GetMaxLabelWidth(signDetailsPropGrid);
            signDetailsPropGrid.MoveSplitterTo(labelWidth + 30);
        }
    }

This is working really well for me.

Upvotes: 1

Χpẘ
Χpẘ

Reputation: 3451

Here is a method that doesn't rely on directly using private methods or reflection. It does still use undocumented interfaces though.

In .NET 4.0 the PropertyGrid.Controls collection contains 4 controls. PropertyGrid.Controls.item(2) is an undocumented PropertyGridView (same type as in answer that uses reflection). The property PropertyGridView.LabelRatio adjusts the relative widths of the columns. The range of LabelRatio looks like it is 1.1 to 9. Smaller values make the left column wider.

I know that setting LabelRatio before you initially display the control works. However I'm not sure what all you need to do to make it take effect once the control is already displayed. You can google MoveSplitterTo to find .NET source code and look at the source for PropertyGridView to get more details. The calculations and operations involving it seem somewhat complicated and I didn't analyze them in detail.

LabelRatio is initially set to 2 (ie splits the available PropertyGrid width in half). Set it to 3 for thirds, 4 for quarter. Code requires Imports System.Reflection

Public Sub MoveVerticalSplitter(grid As PropertyGrid, Fraction As Integer)
    Try
        Dim info = grid.[GetType]().GetProperty("Controls")
        Dim collection = DirectCast(info.GetValue(grid, Nothing), Control.ControlCollection)

        For Each control As Object In collection
            Dim type = control.[GetType]()

            If "PropertyGridView" = type.Name Then
                control.LabelRatio = Fraction

                grid.HelpVisible = True
                Exit For
            End If
        Next

    Catch ex As Exception
        Trace.WriteLine(ex)
    End Try
End Sub

To change size of Description Pane at bottom of PropertyGrid as lines of text

Public Sub ResizeDescriptionArea(grid As PropertyGrid, lines As Integer)
    Try
        Dim info = grid.[GetType]().GetProperty("Controls")
        Dim collection = DirectCast(info.GetValue(grid, Nothing), Control.ControlCollection)

        For Each control As Object In collection
            Dim type = control.[GetType]()

            If "DocComment" = type.Name Then
                Const Flags As BindingFlags = BindingFlags.Instance Or BindingFlags.NonPublic
                Dim field = type.BaseType.GetField("userSized", Flags)
                field.SetValue(control, True)

                info = type.GetProperty("Lines")
                info.SetValue(control, lines, Nothing)

                grid.HelpVisible = True
                Exit For
            End If
        Next

    Catch ex As Exception
        Trace.WriteLine(ex)
    End Try
End Sub

Upvotes: 3

Catalin M.
Catalin M.

Reputation: 652

Yes, unfortunately this requires some reflection based hacks in order to be achieved. Here is a sample extensions class:

PropertyGridExtensionHacks.cs

using System.Reflection;
using System.Windows.Forms;

namespace PropertyGridExtensionHacks
{
    public static class PropertyGridExtensions
    {
        /// <summary>
        /// Gets the (private) PropertyGridView instance.
        /// </summary>
        /// <param name="propertyGrid">The property grid.</param>
        /// <returns>The PropertyGridView instance.</returns>
        private static object GetPropertyGridView(PropertyGrid propertyGrid)
        { 
            //private PropertyGridView GetPropertyGridView();
            //PropertyGridView is an internal class...
            MethodInfo methodInfo = typeof(PropertyGrid).GetMethod("GetPropertyGridView", BindingFlags.NonPublic | BindingFlags.Instance);
            return methodInfo.Invoke(propertyGrid, new object[] {});
        }

        /// <summary>
        /// Gets the width of the left column.
        /// </summary>
        /// <param name="propertyGrid">The property grid.</param>
        /// <returns>
        /// The width of the left column.
        /// </returns>
        public static int GetInternalLabelWidth(this PropertyGrid propertyGrid)
        {
            //System.Windows.Forms.PropertyGridInternal.PropertyGridView
            object gridView = GetPropertyGridView(propertyGrid);

            //protected int InternalLabelWidth
            PropertyInfo propInfo = gridView.GetType().GetProperty("InternalLabelWidth", BindingFlags.NonPublic | BindingFlags.Instance);
            return (int)propInfo.GetValue(gridView);
        }

        /// <summary>
        /// Moves the splitter to the supplied horizontal position.
        /// </summary>
        /// <param name="propertyGrid">The property grid.</param>
        /// <param name="xpos">The horizontal position.</param>
        public static void MoveSplitterTo(this PropertyGrid propertyGrid, int xpos)
        {
            //System.Windows.Forms.PropertyGridInternal.PropertyGridView
            object gridView = GetPropertyGridView(propertyGrid);

            //private void MoveSplitterTo(int xpos);
            MethodInfo methodInfo = gridView.GetType().GetMethod("MoveSplitterTo", BindingFlags.NonPublic | BindingFlags.Instance);
            methodInfo.Invoke(gridView, new object[] { xpos });
        }
    }
}

To move the splitter position use the MoveSplitterTo extension method. Use the GetInternalLabelWidth extension method to get the actual position of the splitter. Please note that I observed that until the SelectedObject is assigned and the PropertyGrid was not shown, GetInternalLabelWidth returns (-1).

Sample use:

using PropertyGridExtensionHacks;
//...

    private void buttonMoveSplitter_Click(object sender, EventArgs e)
    {
        int splitterPosition = this.propertyGrid1.GetInternalLabelWidth();
        this.propertyGrid1.MoveSplitterTo(splitterPosition + 10);
    }

Upvotes: 12

Related Questions