Hi @Nico Zhu (Shanghai Wicresoft Co,.Ltd.) ,
I decided to send a 'dummy' answer rather than use Github or OneDrive, so here it is:
The circular progress bar is implemented as a User Control in a UWP project. The Xaml for it is as follows:
<UserControl
x:Class="UWPCircTest1.RoundProgressControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:UWPCircTest1"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="400" Loaded="UserControl_Loaded" SizeChanged="UserControl_SizeChanged">
<Grid Name="TheGrid">
<Grid.Lights>
<local:TestAmbientLight/>
</Grid.Lights>
<Path x:Name="ThePath" Fill="Transparent" Stroke="Green" StrokeThickness="20" StrokeDashCap="Flat">
<Path.Data>
<PathGeometry>
<PathGeometry.Figures>
<PathFigureCollection>
<PathFigure x:Name="TheFigure" StartPoint="0,0">
<PathFigure.Segments>
<PathSegmentCollection>
<ArcSegment x:Name="TheSegment" IsLargeArc="False" SweepDirection="Clockwise" Point="0,0" />
</PathSegmentCollection>
</PathFigure.Segments>
</PathFigure>
</PathFigureCollection>
</PathGeometry.Figures>
</PathGeometry>
</Path.Data>
</Path>
<TextBlock x:Name="ValInd" HorizontalAlignment="Center" VerticalAlignment="Center" Text="Test %"/>
<Ellipse x:Name="OuterCirc" Fill="Transparent" Width="50" Height="50" StrokeThickness="5" Stroke="DodgerBlue"/>
<Ellipse x:Name="InnerCirc" Fill="Transparent" Width="40" Height="40" StrokeThickness="5" Stroke="DodgerBlue"/>
</Grid>
</UserControl>
The 'code behind' does contain a bit of irrelevant debug info but is as follows:
public sealed partial class RoundProgressControl : UserControl
{
bool IsDataInitiated;
public double MaxValue { get; set; }
public double MinValue { get; set; }
private double lastValue;
private double gridWidth;
private double gridHeight;
private Point gridCentre;
private double circ3Rad;
private double rad3X;
private double rad3Y;
private Point _startPoint;
private Point _endPoint;
private Size _circSize;
private double orgGridWidth;
private double orgGridHeight;
private SpotLight myLight;
private const float SpotLightZVal = 50;
public RoundProgressControl()
{
this.InitializeComponent();
IsDataInitiated = false;
}
public void InitInternalData()
{
gridWidth = orgGridWidth = TheGrid.ActualWidth;
gridHeight = orgGridHeight = TheGrid.ActualHeight;
gridCentre.X = gridWidth / 2;
gridCentre.Y = gridHeight / 2;
rad3X = gridWidth / 4.0;
rad3Y = gridHeight / 4.0;
circ3Rad = (rad3X < rad3Y ? rad3X : rad3Y);
_startPoint = new Point(gridCentre.X, gridCentre.Y - circ3Rad);
_endPoint = new Point(_startPoint.X, _startPoint.Y);
_circSize = new Size(circ3Rad, circ3Rad);
TheFigure.StartPoint = _startPoint;
TheSegment.Point = _endPoint;
TheSegment.Size = _circSize;
lastValue = 0.0;
OuterCirc.Height = (circ3Rad * 2.0) + ThePath.StrokeThickness;
OuterCirc.Width = (circ3Rad * 2.0) + ThePath.StrokeThickness;
InnerCirc.Height = (circ3Rad * 2.0) - ThePath.StrokeThickness;
InnerCirc.Width = (circ3Rad * 2.0) - ThePath.StrokeThickness;
var gridVisual = ElementCompositionPreview.GetElementVisual(TheGrid);
var compositor = gridVisual.Compositor;
this.myLight = compositor.CreateSpotLight();
this.myLight.CoordinateSpace = gridVisual;
this.myLight.InnerConeColor = Colors.White;
// Not Quite right
double opp = ((ThePath.StrokeThickness / 2.0) + OuterCirc.StrokeThickness);
double adj = SpotLightZVal;
double tanOppAdj = opp / adj;
double radTanOppAdj = Math.Atan(tanOppAdj);
double angTanOppAdj = radTanOppAdj * (180 / Math.PI);
double outerBit = Math.Atan(((ThePath.StrokeThickness / 2.0) + OuterCirc.StrokeThickness) / SpotLightZVal) * (180 / Math.PI);
double innerBit = Math.Atan(((ThePath.StrokeThickness / 2.0) + InnerCirc.StrokeThickness) / SpotLightZVal) * (180 / Math.PI);
this.myLight.InnerConeAngleInDegrees = (float)(outerBit + innerBit);
this.myLight.OuterConeAngleInDegrees = (float)(outerBit + innerBit);
this.myLight.Offset = new Vector3((float)_startPoint.X, (float)_startPoint.Y, SpotLightZVal);
this.myLight.Targets.Add(gridVisual);
IsDataInitiated = true;
}
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
InitInternalData();
}
public void SetCircLength(double value)
{
StringBuilder sb = new StringBuilder();
//double realValue = newValue - oldValue;
lastValue = value;
double angle = 2 * Math.PI * (value / MaxValue);
//sb.AppendFormat("value = {0} - angle = {1}", value, angle);
//System.Diagnostics.Debug.WriteLine(sb.ToString());
double X = gridCentre.X + (Math.Sin(angle) * circ3Rad);
double Y = gridCentre.Y - (Math.Cos(angle) * circ3Rad);
if (value > 0)
{
// Math.Round() is producing the wrong number - there's a tiny difference between X and _startPoint.X and
// it's making the difference big by cpmparison. Sort this out!! - Sorted!!
// Check if circle is about to be complete - Circle disappears if start point == end point
int x1 = (int)Math.Round(X, 5, MidpointRounding.AwayFromZero);
int x2 = (int)Math.Round(_startPoint.X, 5, MidpointRounding.AwayFromZero);
int y1 = (int)Math.Round(Y, 5, MidpointRounding.AwayFromZero);
int y2 = (int)Math.Round(_startPoint.Y, 5, MidpointRounding.AwayFromZero);
if (x1 == x2 && y1 == y2)
{
X -= 0.01; // Was +=, Now -= reduces X by 0.01
}
}
// Run this on the UI thread because the IsLargeArc and Point values need to get set only in that thread.
IAsyncAction TheTask =
CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.High,
() =>
{
_endPoint.X = X;
_endPoint.Y = Y;
TheSegment.IsLargeArc = angle >= 3.14159265;
TheSegment.Point = new Point(X, Y);
sb.AppendFormat("{0}%", CalcPercent(value / MaxValue));
ValInd.Text = sb.ToString();
myLight.Offset = new Vector3((float)X, (float)Y, myLight.Offset.Z);
});
}
private double CalcPercent(double _val)
{
double retVal = (_val / 1.00 * 100);
return retVal;
}
private void UserControl_SizeChanged(object sender, SizeChangedEventArgs e)
{
double c3Rad;
Size c3Size;
double r3X;
double r3Y;
StringBuilder sb = new StringBuilder();
sb.AppendFormat("Centre = {0}, {1} Size = Width {2} , Height {3}",
gridCentre.X,gridCentre.Y,
TheSegment.Size.Width, TheSegment.Size.Height);
System.Diagnostics.Debug.WriteLine(sb.ToString());
if (IsDataInitiated)
{
gridWidth = TheGrid.ActualWidth;
gridHeight = TheGrid.ActualHeight;
gridCentre.X = gridWidth / 2.0;
gridCentre.Y = gridHeight / 2.0;
r3X = gridWidth / 4.0;
r3Y = gridHeight / 4.0;
c3Rad = r3X < r3Y ? r3X : r3Y;
c3Size = new Size(c3Rad, c3Rad);
_circSize = c3Size;
circ3Rad = c3Rad;
TheSegment.Size = _circSize;
if (_startPoint == _endPoint)
{
_startPoint = new Point(gridCentre.X, gridCentre.Y - circ3Rad);
_endPoint = new Point(_startPoint.X, _startPoint.Y);
}
else
{
_startPoint = new Point(gridCentre.X, gridCentre.Y - circ3Rad);
SetCircLength(lastValue);
}
TheFigure.StartPoint = _startPoint;
TheSegment.Point = _endPoint;
myLight.Offset = new Vector3((float)_endPoint.X, (float)_endPoint.Y, myLight.Offset.Z);
OuterCirc.Height = (circ3Rad * 2.0) + ThePath.StrokeThickness;
OuterCirc.Width = (circ3Rad * 2.0) + ThePath.StrokeThickness;
InnerCirc.Height = (circ3Rad * 2.0) - ThePath.StrokeThickness;
InnerCirc.Width = (circ3Rad * 2.0) - ThePath.StrokeThickness;
}
}
}
The control was contained within a test harness that utilised a slider control to provide the progress control's data. Whenever the slider's value changed its 'value changed' handler is called which, in turn' calls the SetCircLength() function on the progress control.
I think this covers everything I've done. As you can see it's all pretty straight forward. it's just the spotlight cone angle I can't get right.