UWP - Renderer experts please help! (Rounded buttons)

PhilipOGormanPhilipOGorman ✭✭✭USMember ✭✭✭

Rounded corners are not supported on UWP Forms. This article expains how to achieve rounded corners on Win Phone:

wintellect.com/devcenter/jprosise/supercharging-xamarin-forms-with-custom-renderers-part-1

This didn't work on UWP, after much digging I was able to get something that almost works, by changing the OnSizeChanged handler:

` private void OnSizeChanged(object sender, EventArgs e)
{
var button = (Xamarin.Forms.Button)sender;

        Control.ApplyTemplate();
        var grids = Control.GetVisuals<Grid>();
        foreach (var grid in grids)
        {
            grid.CornerRadius = new CornerRadius(button.BorderRadius);
        }

        var presenters = Control.GetVisuals<ContentPresenter>();
        foreach (var presenter in presenters)
        {
            presenter.CornerRadius = new CornerRadius(button.BorderRadius);
        }

        button.SizeChanged -= OnSizeChanged;
    }`

In the original code Control.GetVisuals(); never returned any borders.....

Anyway my code works so long as I don't set a background color:

<Button Text="Click Me" BorderRadius="32" BorderWidth="8"/> <Button Text="Click Me2" BorderRadius="32" BorderWidth="8" BackgroundColor="Blue"/>

If I set a background color I'm left with the corners. This is driving me crazy! Any suggestions?

Thanks,
Philip

Posts

  • PhilipOGormanPhilipOGorman ✭✭✭ USMember ✭✭✭

    Any advise? Still stuck on this.......

  • JohnHardmanJohnHardman mod GBUniversity mod

    @PhilipOGorman - The BackgroundColor does include the area outside the border, so even if you put rounded corners on (as follows), the background color remains.

            Control.SetControlBorder(new Windows.UI.Xaml.Thickness(Element.BorderWidth));
            Control.SetControlBorderRadius(Element.BorderRadius);
            var converter = new ColorConverter();
            Control.SetControlBorderColor((SolidColorBrush)
                    converter.Convert(Element.BorderColor, null, null, null));
    

    What you may want to do is to use Color.Transparent for the area outside the border, and a different color for inside the border. One of those should be linked to BackgroundColor, the other should be linked to another attribute on a roundbutton custom control. Although it feels wrong to do it this way, it's probably easier to use BackgroundColor=Transparent for the bit outside the border, and paint the area inside the border yourself.

    This problem with the corners pre-dates UWP - I raised a bug against Xamarin.Forms on WinRT quite a while back, details at:
    https://bugzilla.xamarin.com/show_bug.cgi?id=31670 . That bug has been marked as IN PROGRESS for nearly 7 months :-(

  • PhilipOGormanPhilipOGorman ✭✭✭ USMember ✭✭✭

    @JohnHardman Thanks for the feedback! I'm a little closer I think. But I cannot make my background color transparent from my renderer. Here is an experiment I ran:

    `private void OnSizeChanged(object sender, EventArgs e)
    {
    //disabled for now
    var button = (Xamarin.Forms.Button)sender;

            Control.Background = new SolidColorBrush(Windows.UI.Color.FromArgb(0, 0, 0, 0));
            Control.ApplyTemplate();
            var backgroundColor = button.BackgroundColor;
            button.BackgroundColor = Xamarin.Forms.Color.Transparent;
            //
            var grids = Control.GetVisuals<Grid>();
            foreach (var grid in grids)
            {
                grid.CornerRadius = new CornerRadius(button.BorderRadius);
                grid.Background = new SolidColorBrush(Windows.UI.Color.FromArgb(255, 255, 0, 0)); // red
            }
    
            var presenters = Control.GetVisuals<ContentPresenter>();
            foreach (var presenter in presenters)
            {
                presenter.CornerRadius = new CornerRadius(button.BorderRadius);
                presenter.Background = new SolidColorBrush(Windows.UI.Color.FromArgb(255, 0, 255, 0)); //green
            }
    
            button.SizeChanged -= OnSizeChanged;
        }`
    

    <Button Text="Click Me" BorderRadius="32" BorderWidth="8"/> <Button Text="Click Me2" BorderRadius="32" BorderWidth="8" BackgroundColor="Blue"/>

    As you can see, I have control over everything within the border of my button, but I cannot clear the Background set in the xaml. I may have to create my own button with a custom property called UWPBackground and then fill that background in the renderer.

    Do you have any idea how to set the complete Background color from the renderer?

  • PhilipOGormanPhilipOGorman ✭✭✭ USMember ✭✭✭
    edited February 2016

    @JohnHardman Ok - very close to what I want now, but the pushed color of the button is not correct, it is based on what ever the color of the parent control is - in the images below normally the pressed color of the button should be a lighter blue, but because no background color is set in the button control it is set to gray.

    Anyway here is the code for the rounded button - if anyone has improvements please share!

    `

    [assembly: ExportRenderer(typeof(RoundedButton), typeof(CustomButtonRenderer))]
    namespace SmartCart.UWP.Renderers
    {
        public class CustomButtonRenderer : ButtonRenderer
        {
            protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.Button> e)
            {
                base.OnElementChanged(e);
                var button = (RoundedButton)e.NewElement;
                if (Control != null)
                {
                    if (button.UWPBackground != Xamarin.Forms.Color.Transparent)
                    {
                        button.BackgroundColor = button.UWPBackground;
                    }
                    button.SizeChanged += OnSizeChanged;
                }
    
            }
    
        private void OnSizeChanged(object sender, EventArgs e)
        {
            var button = (RoundedButton)sender;
    
            Control.ApplyTemplate();
    
            var grids = Control.GetVisuals<Grid>();
            foreach (var grid in grids)
            {
                grid.CornerRadius = new CornerRadius(button.BorderRadius);
            }
    
            var presenters = Control.GetVisuals<ContentPresenter>();
            foreach (var presenter in presenters)
            {
                presenter.CornerRadius = new CornerRadius(button.BorderRadius);
            }
    
            button.SizeChanged -= OnSizeChanged;
        }
    
    
        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);
            Debug.WriteLine("OnElementPropertyChanged : " + e.PropertyName);
            if (e.PropertyName == "BorderRadius")
            {
                var borders = Control.GetVisuals<Border>();
    
                foreach (var border in borders)
                {
                    border.CornerRadius = new CornerRadius(((Xamarin.Forms.Button)sender).BorderRadius);
                }
            }
        }
    }
    
    static class DependencyObjectExtensions
    {
        public static IEnumerable<T> GetVisuals<T>(this DependencyObject root)
            where T : DependencyObject
        {
            int count = VisualTreeHelper.GetChildrenCount(root);
            Debug.WriteLine("root is Type: " + root.ToString() + " number of children: " + count.ToString());
            for (int i = 0; i < count; i++)
            {
                var child = VisualTreeHelper.GetChild(root, i);
                Debug.WriteLine("Child is Type: " + child.ToString());
                if (child is T)
                    yield return child as T;
    
                foreach (var descendants in child.GetVisuals<T>())
                {
                    yield return descendants;
                }
            }
        }
    }
    

    }`

    `

    public class RoundedButton : Button
    {
    public Color UWPBackground
    {
    get { return (Color)GetValue(UWPBackgroundProperty); }
    set
    {
    SetValue(UWPBackgroundProperty, value);
    }
    }

            public static readonly BindableProperty UWPBackgroundProperty =
                BindableProperty.Create<RoundedButton, Color>(p => p.UWPBackground, Color.Transparent);
        }`
    

    <controls:RoundedButton Text="Click Me2" BorderRadius="32" BorderWidth="8" UWPBackground="Blue"/>

  • JohnHardmanJohnHardman mod GBUniversity mod

    @PhilipOGorman - Apologies for delay in replying. Will try to take a look later this week.

  • PhilipOGormanPhilipOGorman ✭✭✭ USMember ✭✭✭
    edited February 2016

    @JohnHardman I came across this article button styles

    I wonder if the renderer can load a button style?

  • JohnHardmanJohnHardman mod GBUniversity mod

    @PhilipOGorman - certainly stuff I have read before has made it sound like styles are an easier way of changing how buttons look on Windows (this was from when I was looking to see how to get rid of the margin around the outside).

    My day is quickly running out, but I'm still hoping to take a look at this if I can find time at the weekend.

  • JohnHardmanJohnHardman mod GBUniversity mod

    @PhilipOGorman - If you don't want to go down the style route, you can create within your renderer an instance of a custom control based on Windows.UI.Xaml.Controls.UserControl, containing a Windows.UI.Xaml.Controls.Button. It should be possible to set the Background on that to a Transparent brush and the Opacity to 0. You can then paint over this whatever you need, including a circular Border.

    The XML comment for UserControl says:
    // Summary:
    // Provides the base class for defining a new control that encapsulates related
    // existing controls and provides its own logic.

  • PhilipOGormanPhilipOGorman ✭✭✭ USMember ✭✭✭
    edited February 2016

    @JohnHardman I'm getting closer. By setting the presenter color it is no longer grey (because in my case the parent background is black). However it's still not right though because:

    1) the presenter brush is always on top. So if the grid and presenter color are the same nothing changes when the button is pressed. So to give some effect I make the presenter color the same and add some transparency. But it doesn't look great.
    2) The border when pressed is still grey. I can't figure out how to change it.

    ` private void OnSizeChanged(object sender, EventArgs e)
    {
    var button = (RoundedButton)sender;
    var color = button.UWPBackground;
    Control.ApplyTemplate();

            var grids = Control.GetVisuals<Grid>();
            foreach (var grid in grids)
            {
                grid.CornerRadius = new CornerRadius(button.BorderRadius);
                grid.Background = new SolidColorBrush(Windows.UI.Color.FromArgb(
                (byte)(color.A * 255), (byte)(color.R * 255), (byte)(color.G * 255), (byte)(color.B * 255)));
            }
    
            var presenters = Control.GetVisuals<ContentPresenter>();
            foreach (var presenter in presenters)
            {
                presenter.CornerRadius = new CornerRadius(button.BorderRadius);
                presenter.Background = new SolidColorBrush(Windows.UI.Color.FromArgb(
                (byte)(color.A * 200), (byte)(color.R * 255), (byte)(color.G * 255), (byte)(color.B * 255)));
            }
    
            button.SizeChanged -= OnSizeChanged;
        }`
    

    I've looked in to loading a xaml template - it's not too difficult. The hard part is creating the template. I'll post some code later.

    I'll check out custom control too

  • PhilipOGormanPhilipOGorman ✭✭✭ USMember ✭✭✭

    Here is an example showing how to aplly a control template. I just have to figure out how to create the template I want.

    `
    protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.Button> e)
    {
    base.OnElementChanged(e);
    var button = (RoundedButton)e.NewElement;
    if (Control != null)
    {
    button.SizeChanged += OnSizeChanged;
    Control.Template = GetTemplate();
    Control.ApplyTemplate();
    }

        }
    
        ControlTemplate GetTemplate()
        {
            string dataTemplateXaml = @"<ControlTemplate  xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
                xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml"">
                <Grid>
                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup x:Name=""CommonStates"">
                                <VisualState x:Name=""Normal""/>
                                <VisualState x:Name=""Pressed"">
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty=""(Grid.Row)"" Storyboard.TargetName=""grid"">
                                            <DiscreteObjectKeyFrame KeyTime=""0"">
                                                <DiscreteObjectKeyFrame.Value>
                                                    <x:Int32>1</x:Int32>
                                                </DiscreteObjectKeyFrame.Value>
                                            </DiscreteObjectKeyFrame>
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name=""PointerOver""/>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>
                        <Grid>
                            <Grid.RowDefinitions>
                                <RowDefinition/>
                                <RowDefinition Height=""7*""/>
                                <RowDefinition/>
                            </Grid.RowDefinitions>
                            <Grid Margin=""0"" Grid.RowSpan=""2"" Grid.Row=""1"">
                                <Border
                                        BorderBrush=""{TemplateBinding Background}""
                                        BorderThickness=""1""
                                        CornerRadius=""6""
                                        Background=""{TemplateBinding Background}""/>
                                <Border Background=""Black"" Opacity=""0.25"" BorderBrush=""Black"" CornerRadius=""6""/>
                            </Grid>
                            <Grid x:Name=""grid"" Margin=""0"" Grid.Row=""0"" Grid.RowSpan=""2"">
                                <Border
                                    BorderBrush=""{TemplateBinding Background}""
                                    BorderThickness=""1""
                                    CornerRadius=""6""
                                    Background=""{TemplateBinding Background}""/>
    
                                <ContentPresenter>
                                    <TextBlock
                                       FontFamily=""{TemplateBinding FontFamily}""
                                        SelectionHighlightColor=""{TemplateBinding Foreground}""
                                        FontSize=""{TemplateBinding FontSize}""
                                        Foreground=""{TemplateBinding Foreground}""
                                        HorizontalAlignment=""Center""
                                        VerticalAlignment=""Center""
                                        Height=""Auto""
                                        Width=""Auto""
                                        Text=""{Binding Content, RelativeSource={RelativeSource Mode=TemplatedParent}}""/>
                                </ContentPresenter>
                            </Grid>
                       </Grid>
                    </Grid>
              </ControlTemplate>";
    
            return (ControlTemplate)XamlReader.Load(dataTemplateXaml);
        }`
    
  • PhilipOGormanPhilipOGorman ✭✭✭ USMember ✭✭✭

    @JohnHardman I was successfully able to load a style from the App.xaml resources. The code is quite simple.
    The trouble is creating a style to do what I wanted to do, this is not a xamarin issues, I think in UWP it is not straight forward to use the standard button and give it rounded corner. I think I need to take the discussion to a windows forum.
    Right now I have to create a new style for each different button color I want to use. If I specify the color when I add a button I get the nasty corners. Here is the code:

    ` public class CustomButtonRenderer : ButtonRenderer
    {

        protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.Button> e)
        {
            base.OnElementChanged(e);
            var button = (RoundedButton)e.NewElement;
            if (Control != null)
            {
                button.SizeChanged += OnSizeChanged;
            }
    
        }
        private void OnSizeChanged(object sender, EventArgs e)
        {
            var button = (RoundedButton)sender;
            Control.Style = (Style)Application.Current.Resources["BlueRoundedButtonStyle"];
            button.SizeChanged -= OnSizeChanged;
        }
    
    
    }`
    

    <Style TargetType="Button" x:Key="BlueRoundedButtonStyle"> <Setter Property="Background" Value="Blue" /> <Setter Property="Foreground" Value="{ThemeResource SystemControlForegroundBaseHighBrush}"/> <Setter Property="BorderBrush" Value="{ThemeResource SystemControlForegroundTransparentBrush}" /> <Setter Property="BorderThickness" Value="{ThemeResource ButtonBorderThemeThickness}" /> <Setter Property="Padding" Value="8,4,8,4" /> <Setter Property="HorizontalAlignment" Value="Left" /> <Setter Property="VerticalAlignment" Value="Center" /> <Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" /> <Setter Property="FontWeight" Value="Normal" /> <Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" /> <Setter Property="UseSystemFocusVisuals" Value="True" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="Button"> <Grid x:Name="RootGrid" Background="{TemplateBinding Background}" CornerRadius="6"> <VisualStateManager.VisualStateGroups> <VisualStateGroup x:Name="CommonStates"> <VisualState x:Name="Normal"> <Storyboard> <PointerUpThemeAnimation Storyboard.TargetName="RootGrid" /> </Storyboard> </VisualState> <VisualState x:Name="PointerOver"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush"> <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightBaseMediumLowBrush}" /> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground"> <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightBaseHighBrush}" /> </ObjectAnimationUsingKeyFrames> <PointerUpThemeAnimation Storyboard.TargetName="RootGrid" /> </Storyboard> </VisualState> <VisualState x:Name="Pressed"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="RootGrid" Storyboard.TargetProperty="Background"> <DiscreteObjectKeyFrame KeyTime="0" Value="LightBlue" /> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush"> <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightTransparentBrush}" /> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground"> <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightBaseHighBrush}" /> </ObjectAnimationUsingKeyFrames> <PointerDownThemeAnimation Storyboard.TargetName="RootGrid" /> </Storyboard> </VisualState> <VisualState x:Name="Disabled"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="RootGrid" Storyboard.TargetProperty="Background"> <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlBackgroundBaseLowBrush}" /> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground"> <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlDisabledBaseMediumLowBrush}" /> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush"> <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlDisabledTransparentBrush}" /> </ObjectAnimationUsingKeyFrames> </Storyboard> </VisualState> </VisualStateGroup> </VisualStateManager.VisualStateGroups> <ContentPresenter x:Name="ContentPresenter" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Content="{TemplateBinding Content}" ContentTransitions="{TemplateBinding ContentTransitions}" ContentTemplate="{TemplateBinding ContentTemplate}" Padding="{TemplateBinding Padding}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" AutomationProperties.AccessibilityView="Raw" CornerRadius="6"/> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style>

  • JohnHardmanJohnHardman mod GBUniversity mod

    @PhilipOGorman - I haven't used ControlTemplates yet, so cannot comment on that way of doing things.

    After some experimentation, it looks like it might be necessary, after creating a control based on UserControl, to change BorderBrush in handlers for GotFocus and LostFocus to get the desired effect. I'm not an expert on Windows UI - I've been doing a bit of trial-and-error stuff, which isn't really how I like to work. Hope that points you in the right direction - not sure I can help further without spending a lot of time on it.

  • PhilipOGormanPhilipOGorman ✭✭✭ USMember ✭✭✭

    Ok - come back to think Forms is causing the problem. I have the effect I want on a native UWP app here:
    https://bitbucket.org/pogorman_jca/uwpbutton

    If I load this style in a renderer in a Forms app it still has the nasty corners (in the color of the button background).
    When forms creates the button it paints the entire rectangle of the button the specified background color. There is nothing from the renderer I can do to clear that color.

  • AdamMeaneyAdamMeaney ✭✭✭✭✭ USMember ✭✭✭✭✭

    As a suggestion, you could always make you own Renderer that inherits from ViewRenderer and replaces the button renderer.

    You would need to implement the same things as the button to keep it consistent, but then you could just insert an entirely new control instead of using the broken Xamarin button.

  • PhilipOGormanPhilipOGorman ✭✭✭ USMember ✭✭✭

    @AdamMeaney I'm not sure how to do that - something like this?

    `public class CustomButtonRenderer : ViewRenderer<RoundedButton, FormsButton>
    {

        protected override void OnElementChanged(ElementChangedEventArgs<RoundedButton> e)
        {
            base.OnElementChanged(e);
            var button = (RoundedButton)e.NewElement;
            if (Control != null)
            {
                button.SizeChanged += OnSizeChanged;
            }
    
        }`
    

    But the Control of ViewRenderer is private I cannot assign it.
    Do you know of any examples on how to do this?

    Thanks!

  • AdamMeaneyAdamMeaney ✭✭✭✭✭ USMember ✭✭✭✭✭

    It would be kind of like this.

    Just replace the camera crap with an actual Button from Xamarin for the View, and your own implementation for the Renderer.

    If you want to follow their code more, use a decompiler to look at the DLLs and copy as much as you think is useful.

  • PhilipOGormanPhilipOGorman ✭✭✭ USMember ✭✭✭

    @AdamMeaney Thanks!

  • JohnHardmanJohnHardman mod GBUniversity mod

    @AdamMeaney @PhilipOGorman - I think we're suggesting the same thing - a custom control based on Windows.UI.Xaml.Controls.UserControl, containing a Windows.UI.Xaml.Controls.Button. The Xamarin.Forms.Button is an awkward beast, particularly on Windows, so basing a custom control on View is probably a better option.

Sign In or Register to comment.