Search code examples
xamlwindows-store-appswindows-10uwp

Relative width for UI Elements with RelativePanel in XAML with UWP Apps


I want to realize something like

|Image (40% Width)|Text(60% Width)|

that adapts to small screens like

|Image (100%)|
|Text(100%|

I've got the following solution with AdaptiveTrigger and an Grid.

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup x:Name="VisualStateGroup">
            <VisualState x:Name="NarrowView">
                <VisualState.StateTriggers>
                    <AdaptiveTrigger MinWindowWidth="0" />
                </VisualState.StateTriggers>
                <VisualState.Setters>
                    <Setter Target="Image.(Grid.ColumnSpan)" Value="2" />
                    <Setter Target="Text.(Grid.Row)" Value="1" />
                    <Setter Target="Text.(Grid.Column)" Value="0" />
                </VisualState.Setters>
            </VisualState>
            <VisualState x:Name="WideView">
                <VisualState.StateTriggers>
                    <AdaptiveTrigger MinWindowWidth="860" />
                </VisualState.StateTriggers>
                <VisualState.Setters>
                    <Setter Target="Image.(Grid.ColumnSpan)" Value="1" />
                    <Setter Target="Text.(Grid.Row)" Value="0" />
                    <Setter Target="Text.(Grid.Column)" Value="1" />
                </VisualState.Setters>
            </VisualState>
        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="2*"/>
        <ColumnDefinition Width="3*"/>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <Image x:Name="Image" Source="../Image.jpg" />
    <TextBlock x:Name="Text" Grid.Column="1" TextWrapping="WrapWholeWords" Text="Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum." />
    <Border Grid.Row="2" Grid.ColumnSpan="2" Background="Blue">
        <TextBlock Text="Other Content" HorizontalAlignment="Center" VerticalAlignment="Center" />
    </Border>
</Grid>

My question:
Is there a similar solution with the new Realtive Panel provided by Windows 10 UWP Apps?


Solution

  • Let's apply my thoughts about x:Null. That's what I think you mean about how to solve this. Apart there is a weird behavior of the Image using RelativePanel but I added a MaxHeight that can be replaced with the ActualHeight Binding of the Text Control if you need:

    <RelativePanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="VisualStateGroup">
                <VisualState x:Name="NarrowView">
                    <VisualState.StateTriggers>
                        <AdaptiveTrigger MinWindowWidth="0" />
                    </VisualState.StateTriggers>
                    <VisualState.Setters>
                        <Setter Target="Text.(RelativePanel.Below)" Value="Image" />
                        <Setter Target="Content.(RelativePanel.Below)" Value="Text" />
    
                        <Setter Target="Text.(RelativePanel.RightOf)" Value="{x:Null}" />
                    </VisualState.Setters>
                </VisualState>
                <VisualState x:Name="WideView">
                    <VisualState.StateTriggers>
                        <AdaptiveTrigger MinWindowWidth="860" />
                    </VisualState.StateTriggers>
                    <VisualState.Setters>
                        <Setter Target="Text.(RelativePanel.Below)" Value="{x:Null}" />
    
                        <Setter Target="Text.(RelativePanel.RightOf)" Value="Image" />
                        <Setter Target="Content.(RelativePanel.Below)" Value="Image" />
                    </VisualState.Setters>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
    
        <Image x:Name="Image" Source="ms-appx:///Assets/StoreLogo.png" MaxWidth="200" />
        <TextBlock x:Name="Text" RelativePanel.Below="Image"  TextWrapping="WrapWholeWords" Text="Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum." />
        <Border x:Name="Content" Background="Blue" RelativePanel.Below="Text" >
        <TextBlock Text="Other Content" HorizontalAlignment="Center" VerticalAlignment="Center" />
        </Border>
    </RelativePanel>
    

    The trick is setting things to {x:Null} it gives a weird warning but it works, I will also update my codeproject article with that.

    I hope is the answer you were looking for.

    UPDATE: The RelativeSizes:

    1.- I create custom attached properties to be able to set relative size to a container:

        public class RelativeSize : DependencyObject
        {
        private static List<FrameworkElement> elements = new List<FrameworkElement>();
    
        private static FrameworkElement Container = null;
        private static bool containerready = false;
    
        public static void SetContainer(UIElement element, FrameworkElement value)
        {
            element.SetValue(ContainerProperty, value);
        }
        public static FrameworkElement GetContainer(UIElement element)
        {
            return (FrameworkElement)element.GetValue(ContainerProperty);
        }
        public static readonly DependencyProperty ContainerProperty =
            DependencyProperty.RegisterAttached("Container", typeof(FrameworkElement), typeof(RelativeSize), new PropertyMetadata(null,ContainerChanged));
    
        private static void ContainerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            Container = (e.NewValue as FrameworkElement);
            Container.SizeChanged += (sc, ec) =>
                {
    
                    foreach (var element in elements)
                    {
                        var rWidth = element.GetValue(RelativeSize.WidthProperty);
    
                        if (rWidth != null)
                        {
                            element.Width = (double)rWidth * Container.ActualWidth;
                        }
                    }
                };
            containerready = true;
        }
    
        public static void SetWidth(UIElement element, double value)
        {
            element.SetValue(WidthProperty, value);
        }
        public static double GetWidth(UIElement element)
        {
            return (double)element.GetValue(WidthProperty);
        }
        public static readonly DependencyProperty WidthProperty =
          DependencyProperty.RegisterAttached("Width", typeof(double), typeof(RelativeSize), new PropertyMetadata(0.0, WidthChanged));
    
        private static async void WidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            while (!containerready)
                await Task.Delay(60);
    
            var fe = d as FrameworkElement;
            if(fe!=null)
            {
                if (!elements.Contains(fe))
                    elements.Add(fe);
                fe.Width = (double)e.NewValue * Container.ActualWidth;
            }
        }
        }
    

    2.- With that you can set:

     xmlns:p="using:Controls.Views.Properties"
    

    ...

     <Image x:Name="Image" p:RelativeSize.Container="{Binding ElementName=Root}" p:RelativeSize.Width="0.4"  Source="ms-appx:///Assets/StoreLogo.png"  />
        <TextBlock x:Name="Text" RelativePanel.Below="Image" p:RelativeSize.Width="0.6" HorizontalAlignment="Left" TextWrapping="WrapWholeWords" Text="Lorem ipsum ..." />
    

    UPDATE2 : The custom attached properties

    XAML:

    <VisualStateGroup x:Name="VisualStateGroup" CurrentStateChanged="VisualStateGroup_CurrentStateChanged">
    

    Code:

    private void VisualStateGroup_CurrentStateChanged(object sender, VisualStateChangedEventArgs e)
    {
        foreach (var sbase in e.NewState.Setters)
        {
            var setter = sbase as Setter;
            var spath = setter.Target.Path.Path;
            var element = setter.Target.Target as FrameworkElement;
    
            if (spath.Contains(nameof(RelativeSize)))
            {
                string property = spath.Split('.').Last().TrimEnd(')');
    
                var prop = typeof(RelativeSize).GetMethod($"Set{property}");
    
                prop.Invoke(null, new object[] { element, setter.Value });
            }
        }
    }
    

    It is a solution for this custom attached property and you can adapt with more custom attached properties, using reflection in the namespace and get all and find by name, but for this is enough.