Windows 10 UWP–Migrating a Windows 8.1 App (Part 7 of N)

Following on from this growing series of posts;

Windows 10 UWP–Migrating a Windows 8.1 App (Part 1 of N where N tends to infinity)

Windows 10 UWP–Migrating a Windows 8.1 App (Part 2 of N)

Windows 10 UWP–Migrating a Windows 8.1 App (Part 3 of N)

Windows 10 UWP–Migrating a Windows 8.1 App (Part 4 of N)

Windows 10 UWP–Migrating a Windows 8.1 App (Part 5 of N)

Windows 10 UWP–Migrating a Windows 8.1 App (Part 6 of N)

Following on from my previous post, I ran the app as it stands on a Dell Venue Pro 8 and was relatively pleased with the results – the app works, detects QR codes and the UI all seemed to scale ok for that smaller screen.

However, I kept coming back to that CameraOptionsUI class and, here’s the reasons. In my code at present, there’s a button to change camera settings which displays (on Surface Pro 3);

image

It lets me adjust the brightness, contrast, flicker compensation and the exposure but, to be honest, not in a particularly lovely way. It’s functional but then when I look at the Camera app on Windows 10 it does it a different way.

In that app, if I click the little ‘more settings’ button at the top of the screen then I go into ‘Pro’ mode;

image

and this nice little wheel pops out from the right hand side of the screen which (on Surface Pro 3) allows me to adjust brightness only in so far as I can tell;

image

That’s nice. It’s not 100% clear to me why the wheel only seems to let me adjust brightness when the CameraOptionsUI does brightness, contrast, exposure and flicker but never mind.

If I do the same thing on my Dell Venue Pro 8 then I see more options in my app from the CameraOptionsUI – specifically I see focus;

image

and if I use the Camera app then I get offered a lot more options than on the Surface Pro 3;

image

options like sensitivity, shutter speed, focus, white balance.

For my simple app, I’m not sure that I want to go any further than perhaps just offering some settings for focus (if the camera/driver can do it). Maybe offer the user one of a set of presets (if available) and/or manual focusing.

However, how should I look to present this?

On the one hand, the CameraOptionsUI mechanism feels a bit clunky and it’s not offered on anything other than desktop/PC (i.e. not phone).

On the other hand, it feels like a lot of work has gone into this control in the built-in camera app;

image

and a user would be familiar with how to use it and it’s really nice to use and yet it’s not a control that’s available in the platform as far as I know and I think that’s a bit of a shame as offering a control like this would make it far easier for developers to build camera apps without having to reinvent a wheel.

With that in mind, I figured that I’d build my own very cheap and cheerful version of a wheel control like the one on the left that I might then use to achieve something a little like this in my own app.

I toyed with whether it was going to be possible to re-template a XAML slider to achieve this but decided against it. I also toyed with whether to write a Control or a UserControl and, for the moment, I’ve written a user control.

The idea of the control is that it displays a circle where some arc on that circle (from a MinAngle to a MaxAngle) represents a range of values from Minimum to Maximum (let’s say 0 to 100) and the control has a current Value (let’s say 50). The control also displays some ThumbContent to represent where the current value is positioned on the circle and it’s possible to drag that content around to change the value which will snap to the nearest Increment value (let’s say 1).

You can see this control (mostly) working in the video below where I’ve positioned it half off the screen;

and that is represented by this XAML;

 <Grid
    Background="Black">
    <Image
      Source="Assets/img3.jpg"
      Stretch="UniformToFill" />
    <TextBlock
      x:Name="textBlock"
      HorizontalAlignment="Right"
      TextWrapping="Wrap"
      FontSize="64"
      Foreground="White"
      RelativePanel.LeftOf="rvs"
      RelativePanel.AlignVerticalCenterWith="rvs"
      Text="{Binding ElementName=rvs,Path=Value}"
      VerticalAlignment="Center"
      Margin="0,0,300,0" />
    <RelativePanel
      HorizontalAlignment="Right"
      VerticalAlignment="Center"
      Margin="0,0,-300,0">

      <ControlsLibrary:RadialValueSelector
        x:Name="rvs"
        Width="600"
        Height="600"
        Foreground="#FF817878"
        Background="#992B2B2B"
        MinAngle="225"
        MaxAngle="315"
        Value="0"
        Minimum="0"
        Maximum="100"
        Increment="1"
        HorizontalAlignment="Right"
        d:LayoutOverrides="Width">
        <ControlsLibrary:RadialValueSelector.ThumbContent>
          <Grid>
            <Ellipse
              Fill="Black"
              Width="80"
              Height="80" />
            <SymbolIcon
              Symbol="Camera" />
          </Grid>
        </ControlsLibrary:RadialValueSelector.ThumbContent>
      </ControlsLibrary:RadialValueSelector>

    </RelativePanel>

  </Grid>

with the control being the RadialValueSelector and it working on an arc from 225 degrees to 315 degrees to display values from 0 to 100 in increments of 1 with a transparent background and a light foreground and with the thumb content being an ellipse and a camera icon.

I haven’t fully finished this yet and it has limitations around the arcs that it can display and also around the control assuming that it is square! in dimensions but it seems to work reasonably well for my needs.

I also wanted it to be able to display enum values and so here’s a second use of the control where it is displaying enum values from the Windows.Media.Devices.FocusMode enum given to it in a slightly cumbersome manner. I’ve changed the fill to be yellow and the thumb to be a rounded rectangle filled in red;

and the XAML for this;


  <Grid
    Background="Black">
    <Grid.Resources>
      <x:String
        x:Key="enumType">
        Windows.Media.Devices.FocusMode, Windows, Version=255.255.255.255, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime
      </x:String>
    </Grid.Resources>
    <Image
      Source="Assets/img3.jpg"
      Stretch="UniformToFill" />
    <TextBlock
      x:Name="textBlock"
      TextWrapping="Wrap"
      FontSize="64"
      Foreground="White"
      Text="{Binding ElementName=rvs,Path=EnumValueName}"
      VerticalAlignment="Top"
      HorizontalAlignment="Center"/>

    <ControlsLibrary:RadialValueSelector
      x:Name="rvs"
      Width="400"
      Height="400"
      Foreground="Black"
      Background="#33AAA26B"
      HorizontalAlignment="Center"
      VerticalAlignment="Top"
      Margin="0,80,0,0"
      MinAngle="90"
      MaxAngle="180"
      EnumTypeName="{StaticResource enumType}">
      <ControlsLibrary:RadialValueSelector.ThumbContent>
        <Grid Opacity="0.5">
          <Rectangle
            Fill="Red"
            Width="120"
            Height="120"
            RadiusX="10"
            RadiusY="10"/>
        </Grid>
      </ControlsLibrary:RadialValueSelector.ThumbContent>
    </ControlsLibrary:RadialValueSelector>
  </Grid>

as I say, I’m not quite done on this control and I’m sure that there are bugs in it but I thought I’d share the control itself at this point in case you want to take it, make something better or similar or whatever. The control is just a user control and its XAML definition is;

<UserControl
  x:Name="userControl"
  x:Class="ControlsLibrary.RadialValueSelector"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="using:ControlsLibrary"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  mc:Ignorable="d"
  d:DesignHeight="800"
  d:DesignWidth="800"
  Loaded="OnLoaded"
  PointerMoved="OnPointerMoved"
  PointerReleased="OnPointerReleased">

  <Grid
    x:Name="outerRadialGrid">
    <Grid.RowDefinitions>
      <RowDefinition
        Height="2*" />
      <RowDefinition
        Height="8*" />
      <RowDefinition
        Height="2*" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
      <ColumnDefinition
        Width="2*" />
      <ColumnDefinition
        Width="8*" />
      <ColumnDefinition
        Width="2*" />
    </Grid.ColumnDefinitions>
    <Ellipse
      Stroke="{Binding Foreground, ElementName=userControl, Mode=OneWay}"
      Grid.ColumnSpan="3"
      Grid.RowSpan="3"
      Fill="{Binding Background, ElementName=userControl}" />
    <Ellipse
      x:Name="innerRadius"
      Stroke="{Binding Foreground, ElementName=userControl}"
      Grid.Column="1"
      Grid.Row="1" />
    <Grid
      x:Name="thumbGrid"
      Margin="0"
      PointerPressed="OnThumbPointerPressed"
      Grid.RowSpan="3"
      Grid.ColumnSpan="3"
      HorizontalAlignment="Center"
      VerticalAlignment="Center"
      RenderTransformOrigin="0.5,0.5">
      <Grid.RenderTransform>
        <TranslateTransform
          x:Name="thumbGridTranslate" />
      </Grid.RenderTransform>

      <ContentPresenter
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch"
        Content="{Binding ElementName=userControl,Path=ThumbContent}" />
    </Grid>

  </Grid>
</UserControl>

 

and the code that lives with that control is;

namespace ControlsLibrary
{
  using System;
  using System.Diagnostics;
  using Windows.Foundation;
  using Windows.UI.Input;
  using Windows.UI.Xaml;
  using Windows.UI.Xaml.Controls;
  using Windows.UI.Xaml.Input;
  using System.Linq;
  using System.Collections.Generic;

  public sealed partial class RadialValueSelector : UserControl
  {
    public RadialValueSelector()
    {
      this.InitializeComponent();
    }
    public static DependencyProperty ValueProperty =
      DependencyProperty.Register("Value", typeof(double), typeof(RadialValueSelector),
        new PropertyMetadata(0.0d, OnValueChanged));

    public static DependencyProperty MinAngleProperty =
      DependencyProperty.Register("MinAngle", typeof(double), typeof(RadialValueSelector),
        new PropertyMetadata(225.0d, OnDPAffectingPositionChanged));

    public static DependencyProperty MaxAngleProperty =
      DependencyProperty.Register("MaxAngle", typeof(double), typeof(RadialValueSelector),
        new PropertyMetadata(315.0d, OnDPAffectingPositionChanged));

    public static DependencyProperty MinimumProperty =
      DependencyProperty.Register("Minimum", typeof(double), typeof(RadialValueSelector),
        new PropertyMetadata(0.0d, OnDPAffectingPositionChanged));

    public static DependencyProperty MaximumProperty =
      DependencyProperty.Register("Maximum", typeof(double), typeof(RadialValueSelector),
        new PropertyMetadata(0.0d, OnDPAffectingPositionChanged));

    public static DependencyProperty ThumbContentProperty =
      DependencyProperty.Register("ThumbContent", typeof(object), typeof(RadialValueSelector),
        new PropertyMetadata(null, OnDPAffectingPositionChanged));

    public static DependencyProperty IncrementProperty =
      DependencyProperty.Register("Increment", typeof(double), typeof(RadialValueSelector),
        new PropertyMetadata(1.0d, null));

    public static DependencyProperty EnumTypeNameProperty =
      DependencyProperty.Register("EnumTypeName", typeof(string), typeof(RadialValueSelector),
        new PropertyMetadata(string.Empty, OnEnumTypeNameChanged));

    public static DependencyProperty EnumValueNameProperty =
      DependencyProperty.Register("EnumValueName", typeof(string), typeof(RadialValueSelector),
        null);

    public string EnumValueName
    {
      get
      {
        return ((string)base.GetValue(EnumValueNameProperty));
      }
      set
      {
        base.SetValue(EnumValueNameProperty, value);
      }
    }
    public string EnumTypeName
    {
      get
      {
        return ((string)base.GetValue(EnumTypeNameProperty));
      }
      set
      {
        base.SetValue(EnumTypeNameProperty, value);
      }
    }
    public double Increment
    {
      get
      {
        return ((double)base.GetValue(IncrementProperty));
      }
      set
      {
        base.SetValue(IncrementProperty, value);
      }
    }
    public double Value
    {
      get
      {
        return ((double)base.GetValue(ValueProperty));
      }
      set
      {
        base.SetValue(ValueProperty, value);
      }
    }
    public double MinAngle
    {
      get
      {
        return ((double)base.GetValue(MinAngleProperty));
      }
      set
      {
        base.SetValue(MinAngleProperty, value);
      }
    }
    public double MaxAngle
    {
      get
      {
        return ((double)base.GetValue(MaxAngleProperty));
      }
      set
      {
        base.SetValue(MaxAngleProperty, value);
      }
    }
    public double Minimum
    {
      get
      {
        return ((double)base.GetValue(MinimumProperty));
      }
      set
      {
        base.SetValue(MinimumProperty, value);
      }
    }
    public double Maximum
    {
      get
      {
        return ((double)base.GetValue(MaximumProperty));
      }
      set
      {
        base.SetValue(MaximumProperty, value);
      }
    }
    public object ThumbContent
    {
      get
      {
        return (base.GetValue(ThumbContentProperty));
      }
      set
      {
        base.SetValue(ThumbContentProperty, value);
      }
    }
    double ValueRange
    {
      get
      {
        return (this.Maximum - this.Minimum);
      }
    }
    double AngleRange
    {
      get
      {
        return (this.MaxAngle - this.MinAngle);
      }
    }
    double RadialLength
    {
      get
      {
        return (this.innerRadius.ActualWidth / 2.0d);
      }
    }
    Point InnerRadiusCenter
    {
      get
      {
        return (
          new Point(
            ((this.outerRadialGrid.ActualWidth - this.innerRadius.ActualWidth) / 2.0d) + this.RadialLength,
            ((this.outerRadialGrid.ActualHeight - this.innerRadius.ActualHeight) / 2.0d) + this.RadialLength));
      }
    }
    static void OnEnumTypeNameChanged(DependencyObject sender,
      DependencyPropertyChangedEventArgs args)
    {
      RadialValueSelector control = (RadialValueSelector)sender;
      control.enumType = null;
      control.enumDetails = null;

      string newValue = args.NewValue as string;
      if (!string.IsNullOrEmpty(newValue))
      {
        control.enumType = Type.GetType(newValue);
        control.enumDetails = new List<Tuple<string, double>>();
        var values = Enum.GetValues(control.enumType);
        var names = Enum.GetNames(control.enumType);

        int i = 0;
        foreach (var item in values)
        {
          control.enumDetails.Add(Tuple.Create(names[i], (double)(int)item));
          i++;
        }
        control.Minimum = control.enumDetails.Min(cd => cd.Item2);
        control.Maximum = control.enumDetails.Max(cd => cd.Item2);
        control.Value = 0.0d;
        control.Increment = 1.0d; // assumption.
        control.SetEnumValueToClosestMatch();
      }
    }
    void OnLoaded(object sender, RoutedEventArgs e)
    {
      this.PositionBasedOnValue();
    }
    static void OnValueChanged(DependencyObject sender,
      DependencyPropertyChangedEventArgs args)
    {
      RadialValueSelector control = (RadialValueSelector)sender;
      control.PositionBasedOnValue();

      control.SetEnumValueToClosestMatch();
    }
    void SetEnumValueToClosestMatch()
    {
      if (this.enumDetails != null)
      {
        double min = double.MaxValue;
        string name = string.Empty;

        // can never figure out how to do this with min.
        foreach (var item in this.enumDetails)
        {
          if (Math.Abs(item.Item2 - this.Value) < min)
          {
            name = item.Item1;
            min = Math.Abs(item.Item2 - this.Value);
          }
        }
        this.EnumValueName = name;
      }
    }
    static void OnDPAffectingPositionChanged(DependencyObject sender,
      DependencyPropertyChangedEventArgs args)
    {
      RadialValueSelector control = (RadialValueSelector)sender;
      control.PositionBasedOnValue();
    }
    void PositionBasedOnValue()
    {
      this.PositionThumbForAngle(this.GetAngleFromValue(this.Value));
    }
    double GetAngleFromValue(double value)
    {
      return (
        ((value - this.Minimum) /
        this.ValueRange *
        this.AngleRange) + this.MinAngle);
    }
    double GetValueFromAngle(double angle)
    {
      double unCorrectedValue =
        ((angle - this.MinAngle) /
          this.AngleRange *
          this.ValueRange) + this.Minimum;

      // Nudge the value to its nearest multiple of the increment property.
      double rem = unCorrectedValue % this.Increment;
      double div = Math.Floor(unCorrectedValue / this.Increment);

      if (rem < (this.Increment / 2.0d))
      {
        unCorrectedValue = div * this.Increment;
      }
      else
      {
        unCorrectedValue = (div + 1) * this.Increment;
      }
      return (unCorrectedValue);
    }
    void PositionThumbForAngle(double angleDegrees)
    {
      Point thumbPositionPoint = new Point(
        this.InnerRadiusCenter.X + this.RadialLength * Math.Sin(DegreesToRadians(angleDegrees)),
        this.InnerRadiusCenter.Y + this.RadialLength * Math.Cos(DegreesToRadians(angleDegrees)));

      Size thumbSize = new Size(this.thumbGrid.ActualWidth, this.thumbGrid.ActualHeight);

      this.thumbGridTranslate.X = thumbPositionPoint.X - this.InnerRadiusCenter.X;
      this.thumbGridTranslate.Y = (0 - (thumbPositionPoint.Y - this.InnerRadiusCenter.Y));
    }
    void OnThumbPointerPressed(object sender, PointerRoutedEventArgs e)
    {
      // Capture the pointer in case the user wanders off with it.
      this.CapturePointer(e.Pointer);
      this.PointerCaptureLost += this.OnPointerCaptureLost;
      this.dragging = true;
    }
    void OnPointerCaptureLost(object sender, PointerRoutedEventArgs e)
    {
      this.dragging = false;
    }
    void OnPointerMoved(object sender, PointerRoutedEventArgs e)
    {
      if (this.dragging)
      {
        PointerPoint pointerPointRelativeToInnerRadius = e.GetCurrentPoint(this.innerRadius);

        Point positionRelativeToInnerRadiusCenter = new Point(
          pointerPointRelativeToInnerRadius.Position.X - this.RadialLength,
          pointerPointRelativeToInnerRadius.Position.Y - this.RadialLength);

        positionRelativeToInnerRadiusCenter.Y = 0 - positionRelativeToInnerRadiusCenter.Y;

        double angleRadians = Math.Atan(
          positionRelativeToInnerRadiusCenter.X / positionRelativeToInnerRadiusCenter.Y);

        double angleDegrees = RadiansToDegrees(angleRadians);
        double increment = 0.0d;

        if (positionRelativeToInnerRadiusCenter.Y <= 0.0d)
        {
          increment = 180.0d;
        }
        else if ((positionRelativeToInnerRadiusCenter.Y >= 0.0d) &&
          (positionRelativeToInnerRadiusCenter.X <= 0.0d))
        {
          increment = 360.0d;
        }
        angleDegrees += increment;

        if ((angleDegrees >= this.MinAngle) &&
          (angleDegrees <= this.MaxAngle))
        {
          this.Value = this.GetValueFromAngle(angleDegrees);
        }
      }
    }
    void OnPointerReleased(object sender, PointerRoutedEventArgs e)
    {
      this.dragging = false;
    }
    static double RadiansToDegrees(double radians)
    {
      return (radians / Math.PI * 180.0d);
    }
    static double DegreesToRadians(double degrees)
    {
      return (degrees * Math.PI / 180.0d);
    }
    bool IsDisplayingEnum
    {
      get
      {
        return (this.enumType != null);
      }
    }
    List<Tuple<string, double>> enumDetails;
    bool dragging = false;
    Type enumType = null;
  }
}

 

Note that it’s been put together fairly quickly and the enum functionality was bolted on last and I doubt that it responds well to switching between ‘enum mode’ and ‘value mode’ but you could perhaps fix it if you want to use it in your own projects.

I’m going to see if I can do something with it in my kwiQR app in the future…