Использование окна просмотра для масштабирования сетки, содержащей метки и текстовые поля

Итак, я пытаюсь создать форму, которая будет автоматически пропорционально масштабироваться вверх и вниз в зависимости от доступной ширины родительского контейнера и тех же процентных соотношений столбцов, например:

«Желаемый

Будет и другой окружающий контент, который также необходимо масштабировать, например изображения и кнопки (которые не будут в одной сетке), и, судя по тому, что я читал до сих пор, использование Viewbox - это лучший способ.

Однако, когда я помещаю свою сетку в окно просмотра с помощью Stretch = "Uniform", текстовое поле контролирует каждое сворачивание до минимальной ширины, которая выглядит следующим образом:  Окно просмотра с сеткой и свернутыми текстовыми полями

Если я увеличиваю ширину контейнера, все масштабируется, как ожидалось (хорошо), но текстовые поля по-прежнему свернуты до минимально возможной ширины (плохо):  Увеличенная ширина

Если я ввожу какой-либо контент в текстовые поля, они увеличивают свою ширину, чтобы содержать текст: Текстовые поля расширяются в соответствии с содержимым

... но я не хочу такого поведения - я хочу, чтобы ширина элемента Textbox была привязана к ширине столбца сетки, а НЕ зависела от содержимого.

Теперь я рассмотрел множество вопросов SO, и этот вопрос ближе всего к тому, что мне нужно: Как автоматически масштабировать размер шрифта для группы элементов управления?

... но он по-прежнему не касался конкретно поведения ширины текстового поля (когда он взаимодействует с поведением окна просмотра), что, по-видимому, является основной проблемой.

Я пробовал множество вещей - разные настройки HorizontalAlignment = "Stretch" и так далее, но пока ничего не помогло. Вот мой XAML:

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="1*" />
            <ColumnDefinition Width="5" />
            <ColumnDefinition Width="2*" />
        </Grid.ColumnDefinitions>

        <StackPanel Grid.Column="0">
            <StackPanel.Background>
                <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                    <GradientStop Color="Silver" Offset="0"/>
                    <GradientStop Color="White" Offset="1"/>
                </LinearGradientBrush>
            </StackPanel.Background>

            <Viewbox Stretch="Uniform" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
                <Grid Background="White">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="1*" />
                        <ColumnDefinition Width="2*" />
                        <ColumnDefinition Width="1*" />
                        <ColumnDefinition Width="2*" />
                    </Grid.ColumnDefinitions>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="*" />
                        <RowDefinition Height="Auto" />
                    </Grid.RowDefinitions>
                    <Label Content="Field A" Grid.Column="0" Grid.Row="0" />
                    <TextBox Grid.Column="1" Grid.Row="0" HorizontalAlignment="Stretch"></TextBox>
                    <Label Content="Field B" Grid.Column="2" Grid.Row="0"  />
                    <TextBox Grid.Column="3" Grid.Row="0" HorizontalAlignment="Stretch"></TextBox>
                </Grid>
            </Viewbox>
            <Label Content="Other Stuff"/>
        </StackPanel>
        <GridSplitter Grid.Column="1" HorizontalAlignment="Stretch" Height="100" Width="5"/>
        <StackPanel Grid.Column="2">
            <Label Content="Body"/>
        </StackPanel>
    </Grid>
</Window>

person jhilgeman    schedule 21.05.2018    source источник
comment
Viewbox используйте размер аранжировки и Child.DesiredSize, чтобы вычислить коэффициент масштабирования дочернего элемента. Поэтому установите подходящую фиксированную ширину и высоту на Grid под Viewbox, все будет просто отлично.   -  person Alex.Wei    schedule 25.05.2018


Ответы (2)


Причина такого поведения в том, что Viewbox дочернему элементу предоставляется бесконечное пространство для измерения его желаемого размера. Растягивать TextBoxes до бесконечности Width не имеет большого смысла, так как это все равно невозможно отрендерить, поэтому возвращается их размер по умолчанию.

Вы можете использовать конвертер, чтобы добиться желаемого эффекта.

public class ToWidthConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        double gridWidth = (double)value;
        return gridWidth * 2/6;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Вы можете все подключить, добавив эти ресурсы.

<Viewbox.Resources>
    <local:ToWidthConverter x:Key="ToWidthConverter"/>
    <Style TargetType="{x:Type TextBox}">
        <Setter Property="Width" 
                Value="{Binding ActualWidth, 
                    RelativeSource={RelativeSource AncestorType={x:Type Grid}}, 
                    Converter={StaticResource ToWidthConverter}}"/>
    </Style>
</Viewbox.Resources>

ОБНОВЛЕНИЕ

У меня проблемы с пониманием исходной проблемы бесконечной ширины сетки.

Подход с бесконечным пространством часто используется для определения DesiredSize из UIElement. Короче говоря, вы предоставляете элементу управления все пространство, которое ему может понадобиться (без ограничений), а затем измеряете его, чтобы получить желаемый размер. Viewbox использует этот подход для измерения своего дочернего элемента, но наш Grid имеет динамический размер (в коде не установлены Height или Width), поэтому Viewbox опускается на другой уровень в дочерних сетках, чтобы увидеть, может ли он определить размер, взяв сумму компонентов.

Однако вы можете столкнуться с проблемами, когда эта сумма компонентов превышает общий доступный размер, как показано ниже.

Вторжение

Я заменил текстовые поля метками Foo и Bar и установил для них серый цвет. Теперь мы видим, что Bar вторгается на Body территорию, это явно не то, что мы хотели сделать.

Опять же, корень проблемы исходит из Viewbox незнания, как разделить бесконечность на 6 равных долей (для сопоставления с шириной столбцов 1*, 2*, _19 _, _ 20_), поэтому все, что нам нужно сделать, это восстановить связь с шириной сетки. В ToWidthConverter цель состояла в том, чтобы сопоставить TextBox 'Width с Grids ColumnWidth из 2*, поэтому я использовал gridWidth * 2/6. Теперь Viewbox может снова решить уравнение: каждый TextBox получает одну треть gridwidth, а каждый Label половину (1* против 2*).

Конечно, когда вы перемешиваете вещи, вводя новые столбцы, вам нужно позаботиться о том, чтобы сумма компонентов была синхронизирована с общей доступной шириной. Другими словами, уравнение должно быть разрешимым. Подсчитайте, сумма желаемых размеров (элементов управления, которые вы не ограничили, метки в нашем примере) и преобразованных размеров (как части gridWidth, текстовых полей в нашем примере) должна быть меньше или равна до доступного размера (gridWidth в нашем примере).

Я обнаружил, что масштабирование работает хорошо, если вы используете преобразованные размеры для TextBoxes и позволяете ColumnWidths размера звезды обрабатывать большинство других. Помня о том, чтобы оставаться в пределах доступного общего размера.

Один из способов добавить гибкости - добавить к смеси ConverterParameter.

public class PercentageToWidthConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        double gridWidth = (double)value;
        double percentage = ParsePercentage(parameter);
        return gridWidth * percentage;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }

    private double ParsePercentage(object parameter)
    {
        // I chose to let it fail if parameter isn't in right format            
        string[] s = ((string)parameter).Split('/');
        double percentage = Double.Parse(s[0]) / Double.Parse(s[1]);
        return percentage;
    }
}

Пример, который делит gridWidth на 10 равных долей и соответственно назначает эти доли компонентам.

<Viewbox Stretch="Uniform">
    <Viewbox.Resources>
        <local:PercentageToWidthConverter x:Key="PercentageToWidthConverter"/>
    </Viewbox.Resources>
    <Grid Background="White">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="2*" />
            <ColumnDefinition Width="2*" />
            <ColumnDefinition Width="2*" />
            <ColumnDefinition Width="3*" />
            <ColumnDefinition Width="1*" />
        </Grid.ColumnDefinitions>
        <Label Content="Field A" Grid.Column="0" />
        <TextBox Grid.Column="1"
                 Width="{Binding ActualWidth, 
                     RelativeSource={RelativeSource AncestorType={x:Type Grid}}, 
                     Converter={StaticResource PercentageToWidthConverter}, 
                     ConverterParameter=2/10}" />
        <Label Content="Field B" Grid.Column="2"  />
        <TextBox Grid.Column="3"
                 Width="{Binding ActualWidth, 
                     RelativeSource={RelativeSource AncestorType={x:Type Grid}}, 
                     Converter={StaticResource PercentageToWidthConverter}, 
                     ConverterParameter=3/10}" />
        <Button Content="Ok" Grid.Column="4"
                Width="{Binding ActualWidth, 
                    RelativeSource={RelativeSource AncestorType={x:Type Grid}}, 
                    Converter={StaticResource PercentageToWidthConverter}, 
                    ConverterParameter=1/10}" />
    </Grid>
</Viewbox>

Обратите внимание на доли для каждого элемента управления, сгруппированные как 2 - 2 - 2 - 3 - 1 (с 1 для ширины кнопки).

Детали

Наконец, в зависимости от того, какую возможность повторного использования вам нужно, можно найти другие способы справиться с этим:

  • Set fixed size(s) on your root Grid. Downsides:
    • Needs to be finetuned each time you change components (to achieve the desired horizontal / vertical / fontsize ratio)
    • Это соотношение может нарушаться для разных тем, версий Windows, ...
  • Добавьте Behavior. Как это сделано в одном из ответов в вашем связанном посте FontSize, но вместо этого реализовано для сопоставления ширины столбцов с частями gridWidth.
  • Создайте индивидуальную панель, как предложено @ Grx70.
person Funk    schedule 24.05.2018
comment
Поэтому, если я добавлю больше пары дополнительных столбцов, этот код перестанет работать. Если я добавлю 2 столбца, сетка полностью исчезнет. Если я добавлю 3 столбца, я получу исключение. Я предполагаю, что мне нужно отрегулировать 2/6, но ничего из того, что я пробовал, еще не работает ... - person jhilgeman; 25.05.2018
comment
Кроме того, когда я наблюдаю за шириной сетки и окна просмотра в отладчике, ширина сетки начинает превышать ширину окна просмотра и продолжает работать до тех пор, пока не возникнет ошибка. - person jhilgeman; 25.05.2018
comment
Если я добавлю сюда жестко заданный предел, например if (gridWidth ›800) {gridWidth = 800; }, тогда он работает, или если я изменю тип предка на Viewbox, тогда это вроде как работает, но у меня возникли проблемы с пониманием исходной проблемы бесконечной ширины сетки. - person jhilgeman; 25.05.2018
comment
@jhilgeman Вы хотите, чтобы сумма ширины синхронизировалась с доступным gridWidth. Жестко запрограммированное значение (2/6) было просто доказательством концепции, добавило некоторые пояснения и гибкость. - person Funk; 25.05.2018
comment
@Funk: При всем уважении, но заметили ли вы, что ваше решение приведет к многократному измерению и упорядочиванию повторов, пока ширина всего столбца не станет больше, чем его DesiredSize? Фактически, если поместить ваш образец кода в окно 525 * 350, ваш преобразователь будет вызываться 204 раза (то есть 67 раз измерения и упорядочения). Изменение соотношения сторон или содержимого повлияет на время повтора, но вызовет серьезные проблемы с производительностью InitializeComponent. Кроме того, каждый элемент куломна должен использовать конвертер, ваш образец показывает хороший результат только потому, что неудачные поля A и B. - person Alex.Wei; 26.05.2018
comment
@ Alex.Wei Решение уравнения требует нескольких проходов, хотя в этом случае ошибки округления вызвали большую часть ошибок. Мне удалось пройти от 201 до ›24 посещений, используя return (int)(gridWidth * percentage);. Кроме того, конвертер необязателен. Вы можете проверить, сбросив Width назначение на Button. - person Funk; 29.05.2018

Проблема с вашим подходом в том, что Grid работает не так, как мы интуитивно думаем. А именно, размер звезды работает должным образом только при соблюдении следующих условий:

  • Grid имеет горизонтальное выравнивание, равное Stretch.
  • Grid содержится в контейнере конечного размера, т.е. его метод Measure получает ограничение с конечным Width (не double.PositiveInfinity)

Это относится к размеру столбца; Калибровка рядов симметрична. В вашем случае второе условие не выполняется. Мне не известны какие-либо простые приемы, позволяющие заставить Grid работать так, как вы ожидаете, поэтому моим решением было бы создать собственный Panel, который бы выполнял эту работу. Таким образом, вы полностью контролируете расположение элементов управления. Это не так уж и сложно, хотя для этого требуется некоторый уровень понимания того, как работает система макета WPF.

Вот пример реализации вашего предложения. Для краткости он работает только по горизонтали, но нетрудно расширить его, чтобы работать и по вертикали.

public class ProportionalPanel : Panel
{
    protected override Size MeasureOverride(Size constraint)
    {
        /* Here we measure all children and return minimal size this panel
         * needs to be to show all children without clipping while maintaining
         * the desired proportions between them. We should try, but are not
         * obliged to, fit into the given constraint (available size) */

        var desiredSize = new Size();
        if (Children.Count > 0)
        {
            var children = Children.Cast<UIElement>().ToList();
            var weights = children.Select(GetWeight).ToList();
            var totalWeight = weights.Sum();
            var unitWidth = 0d;
            if (totalWeight == 0)
            {
                //We should handle the situation when all children have weights set
                //to 0. One option is to measure them with 0 available space. To do
                //so we simply set totalWeight to something other than 0 to avoid
                //division by 0 later on.
                totalWeight = children.Count;

                //We could also assume they are to be arranged uniformly, so we
                //simply coerce their weights to 1
                for (var i = 0; i < weights.Count; i++)
                    weights[i] = 1;
            }
            for (var i = 0; i < children.Count; i++)
            {
                var child = children[i];
                child.Measure(new Size
                {
                    Width = constraint.Width * weights[i] / totalWeight,
                    Height = constraint.Height
                });
                desiredSize.Width += child.DesiredSize.Width;
                desiredSize.Height =
                    Math.Max(desiredSize.Height, child.DesiredSize.Height);
                if (weights[i] != 0)
                    unitWidth =
                        Math.Max(unitWidth, child.DesiredSize.Width / weights[i]);
            }
            if (double.IsPositiveInfinity(constraint.Width))
            {
                //If there's unlimited space (e.g. when the panel is nested in a Viewbox
                //or a StackPanel) we need to adjust the desired width so that no child
                //is given less than desired space while maintaining the desired
                //proportions between them
                desiredSize.Width = totalWeight * unitWidth;
            }
        }
        return desiredSize;
    }

    protected override Size ArrangeOverride(Size constraint)
    {
        /* Here we arrange all children into their places and return the
         * actual size this panel is. The constraint will never be smaller
         * than the value of DesiredSize property, which is determined in 
         * the MeasureOverride method. If the desired size is larger than
         * the size of parent element, the panel will simply be clipped 
         * or appear "outside" of the parent element */

        var size = new Size();
        if (Children.Count > 0)
        {
            var children = Children.Cast<UIElement>().ToList();
            var weights = children.Select(GetWeight).ToList();
            var totalWeight = weights.Sum();
            if (totalWeight == 0)
            {
                //We perform same routine as in MeasureOverride
                totalWeight = children.Count;
                for (var i = 0; i < weights.Count; i++)
                    weights[i] = 1;
            }
            var offset = 0d;
            for (var i = 0; i < children.Count; i++)
            {
                var width = constraint.Width * weights[i] / totalWeight;
                children[i].Arrange(new Rect
                {
                    X = offset,
                    Width = width,
                    Height = constraint.Height,
                });
                offset += width;
                size.Width += children[i].RenderSize.Width;
                size.Height = Math.Max(size.Height, children[i].RenderSize.Height);
            }
        }
        return size;
    }

    public static readonly DependencyProperty WeightProperty =
        DependencyProperty.RegisterAttached(
            name: "Weight",
            propertyType: typeof(double),
            ownerType: typeof(ProportionalPanel),
            defaultMetadata: new FrameworkPropertyMetadata
            {
                AffectsParentArrange = true, //because it's set on children and is used
                                             //in parent panel's ArrageOverride method
                AffectsParentMeasure = true, //because it's set on children and is used
                                             //in parent panel's MeasuerOverride method
                DefaultValue = 1d,
            },
            validateValueCallback: ValidateWeight);

    private static bool ValidateWeight(object value)
    {
        //We want the value to be not less than 0 and finite
        return value is double d
            && d >= 0 //this excludes double.NaN and double.NegativeInfinity
            && !double.IsPositiveInfinity(d);
    }

    public static double GetWeight(UIElement d)
        => (double)d.GetValue(WeightProperty);

    public static void SetWeight(UIElement d, double value)
        => d.SetValue(WeightProperty, value);
}

И использование выглядит так:

<local:ProportionalPanel>
    <Label Content="Field A" local:ProportionalPanel.Weight="1" />
    <TextBox local:ProportionalPanel.Weight="2" />
    <Label Content="Field B" local:ProportionalPanel.Weight="1" />
    <TextBox local:ProportionalPanel.Weight="2" />
</local:ProportionalPanel>
person Grx70    schedule 25.05.2018