Использование Viewbox для масштабирования сетки, содержащей метки и текстовые поля - PullRequest
0 голосов
/ 22 мая 2018

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

Desired result

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

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

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

Если я введу какое-либо содержимое в текстовые поля, они увеличат свою ширину до текста: Textboxes expand to fit content

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

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

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

Я пробовал разные вещи - разные настройки 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>

Ответы [ 2 ]

0 голосов
/ 25 мая 2018

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

  • Горизонтальное выравнивание 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>
0 голосов
/ 24 мая 2018

Причиной такого поведения является то, что дочернему элементу Viewbox предоставляется бесконечное пространство для измерения его желаемого размера.Растягивание TextBox es до бесконечного 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 опускается еще на один уровень в сетках, чтобы видеть детейесли он может определить размер, взяв сумму компонентов.

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

Invade

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

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

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

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

Один из способов повысить гибкость - добавить 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 для ширины кнопки).

Parts

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

  • Установить фиксированные размеры для вашего корня Grid.Недостатки:
    • Необходима точная настройка при каждом изменении компонентов (для достижения желаемого соотношения горизонтального / вертикального / размера шрифта)
    • Это соотношение может нарушаться в разных темах, версиях Windows, ...
  • Добавить Behavior.Как и в одном из ответов в вашем связанном сообщении FontSize, но вместо этого реализовано отображение ширины столбца в части gridWidth.
  • Создание пользовательской панели, как предложено @ Grx70.
...