前一篇簡單的利用了既有的固定座標繪製弧形,以進度環的觀點來看,基本上應該是以夾角的角度來計算才對,這會應用到數學上的直角座標與極座標轉換。
座標公式
假設已知圓心的點座標為 (x0, y0)、半徑為 r 、夾角為 θ。則在此圓上的任一點 (x1,y1) 的公式為以下:
x1 = x0 + r * cos ( θ * π / 180 )
y1 = y0 + r * sin ( θ * π / 180 )
極座標系的角度是從 3 點鐘方向 ( 0 度) 逆時鐘增加,可參考下圖
設定角度換算螢幕座標
依據上述的公式,我們要先決定中心點的位置,如果在一個寬高為 (width, height) ,正方形區域內繪製一個半徑為 ( width/2, height/2 ) 的弧形,圓心的座標為 ( width/2, height/2 ) 。
12 點鐘方向(夾角 90度) 換算為直角座標系的起點應為:
x= (width/2) + (width/2) * cos ( 90 * π / 180) ,其中 cos ( π / 2) = 0,故 x = (width/2)
y= (height/2) + (height/2) * sin ( 90 * π / 180) ,其中 sin ( π / 2) = 1,故 y = height
這邊有一個問題,這裡換算出來的直角座標系是上面圖二的座標系,這種座標系是由左下方往右上方增加,但螢幕的座標系是從左上角往右下方增加的;所以 y 座標要做一些修正。以這個例子直角座標系的 y = 0 是螢幕座標系的 y = height,所以再度換算成螢幕座標系:
y= height - ((height/2) + (height/2) * sin ( 90 * π / 180)) = 0。
得出在螢幕上的 12 點鐘方向起點為 ( width/2 , 0 )。
3 點鐘方向換算為螢幕座標系為:
x = (width/2) + (width/2) * cos ( 0 * π / 180) = (width/2) + (width/2) * 1 = width
y = height - ((height/2) + (height/2) * sin ( 0 * π / 180)) = height - ((height/2) + (height/2) * 0) = height - (height/2) = height/2
角度換算
因為起點是極座標的 90 度,要順時鐘繞圈,假設要移動角度為 α ( 0 < α ≤ 360 ),若 0 < α ≤ 90 ,則極座標的 θ = 90 - α;若在前述範圍外則 θ = (90 - α)+ 360。也就是說如果終點的位置是對應起點 180 度,換算成極座標的角度就是 90 - 180 + 360 = 270 度,說白了就是 90 - α 只要 < 0 就加上 360。
完成程式碼
(1) 為終點角度設定一個依賴屬性
public static readonly DependencyProperty EndAngleProperty =
DependencyProperty.Register(nameof(EndAngle), typeof(double), typeof(Arc), new PropertyMetadata(0.0, null, new CoerceValueCallback(OnEndAngleChanged)));
private static object OnEndAngleChanged(DependencyObject d, object baseValue)
{
var value = (double)baseValue;
if (value < 0 ) { value = 0; }
if (value > 359.9) { value = 359.9; }
return value;
}
public double EndAngle
{
get => (double)GetValue(EndAngleProperty);
set => SetValue(EndAngleProperty, value);
}
這段程式碼比較特別的地方是用到了 CoerceValueCallback,當依賴屬性的值被設定的時候會呼叫這個委派,它將外部設定的值透過 baseValue 參數傳遞給委派函式,而這個函式的回傳值將會重新設定依賴屬性的值。由於我們將角度限定在 0 ~ 360 度,所以在設定值的時候會做這個檢查,但如果終點設定為 360 度,這個圓的方向會長到不知那兒去,所以將極限設定為 359.9。
(2) 決定是否為 Large Arc
private bool IsLargeArc()
{
return EndAngle > 180;
}
如果角度超過 180 ,則應該是 Large Arc。
(3) 取得螢幕座標
private Point GetCenter()
{
return new Point(RenderSize.Width / 2, RenderSize.Height / 2);
}
private Point GetScreenCoordinate()
{
var angle = EndAngleToPolarAngle();
var center = GetCenter();
double radian = angle * Math.PI / 180.0;
var x = center.X + (RenderSize.Width / 2) * Math.Cos(radian);
var y = RenderSize.Height - (center.Y + (RenderSize.Height / 2) * Math.Sin(radian));
return new Point(x, y);
}
不論從直角坐標或螢幕座標的角度,圓心的座標都會一樣;但是圓上的點就不同了。
(4) 完整 Arc class 程式碼
public class Arc : Shape
{
public static readonly DependencyProperty EndAngleProperty =
DependencyProperty.Register(nameof(EndAngle), typeof(double), typeof(Arc), new PropertyMetadata(0.0, null, new CoerceValueCallback(OnEndAngleChanged)));
private static object OnEndAngleChanged(DependencyObject d, object baseValue)
{
var value = (double)baseValue;
if (value < 0) { value = 0; }
if (value > 359.9) { value = 359.9; }
return value;
}
public double EndAngle
{
get => (double)GetValue(EndAngleProperty);
set => SetValue(EndAngleProperty, value);
}
protected override Geometry DefiningGeometry
{
get
{
StreamGeometry geometry = new StreamGeometry();
using (StreamGeometryContext context = geometry.Open())
{
context.BeginFigure(new Point(RenderSize.Width / 2, 0), false, false);
var end = GetScreenCoordinate();
context.ArcTo(end, new Size(RenderSize.Width / 2, RenderSize.Height / 2), 0.0, IsLargeArc(), SweepDirection.Clockwise, true,true);
}
return geometry;
}
}
private bool IsLargeArc()
{
return EndAngle > 180;
}
private double EndAngleToPolarAngle()
{
var result = 90.0 - EndAngle;
if (result < 0) { result += 360.0; }
return result;
}
private Point GetCenter()
{
return new Point(RenderSize.Width / 2, RenderSize.Height / 2);
}
private Point GetScreenCoordinate()
{
var angle = EndAngleToPolarAngle();
var center = GetCenter();
double radian = angle * Math.PI / 180.0;
var x = center.X + (RenderSize.Width / 2) * Math.Cos(radian);
var y = RenderSize.Height - (center.Y + (RenderSize.Height / 2) * Math.Sin(radian));
return new Point(x, y);
}
}
DefiningGeometry 透過組合上述的幾個方法完成弧形的繪製,來個畫面試試看。
<Border Width="100" Height="100" Background="LightBlue">
<local:Arc Stroke="Brown" StrokeThickness="1" EndAngle="270" />
</Border>
這個階段的範例程式在此,結束了嗎?還沒,如果你將 xaml 中的 StrokeThickness 放大,你會發現弧形很明顯的會突出到 Border 外面,下一步要修正這個影響。
修正 StrokeThickness
當設定 StrokThickness 的時候,線條的粗細是由中心點往兩側擴散,所以要調整一下整個計算公式加入邊框修正:
public class Arc : Shape
{
public static readonly DependencyProperty EndAngleProperty =
DependencyProperty.Register(nameof(EndAngle), typeof(double), typeof(Arc), new PropertyMetadata(0.0, null, new CoerceValueCallback(OnEndAngleChanged)));
private static object OnEndAngleChanged(DependencyObject d, object baseValue)
{
var value = (double)baseValue;
if (value < 0) { value = 0; }
if (value > 359.9) { value = 359.9; }
return value;
}
public double EndAngle
{
get => (double)GetValue(EndAngleProperty);
set => SetValue(EndAngleProperty, value);
}
protected override Geometry DefiningGeometry
{
get
{
StreamGeometry geometry = new StreamGeometry();
using (StreamGeometryContext context = geometry.Open())
{
context.BeginFigure(new Point(RenderSize.Width / 2.0, StrokeThickness / 2.0), false, false);
var end = GetScreenCoordinate();
(double radiusX, double radiusY) = GetRadii();
context.ArcTo(end, new Size(radiusX > 0 ? radiusX : 0, radiusY > 0 ? radiusY : 0), 0.0, IsLargeArc(), SweepDirection.Clockwise, true, true);
}
return geometry;
}
}
private bool IsLargeArc()
{
return EndAngle > 180;
}
private double EndAngleToPolarAngle()
{
var result = 90.0 - EndAngle;
if (result < 0) { result += 360.0; }
return result;
}
private (double radiusX, double radiusY) GetRadii()
{
return ((RenderSize.Width - StrokeThickness) / 2.0 , (RenderSize.Height - StrokeThickness) / 2.0) ;
}
private Point GetScreenCoordinate()
{
var angle = EndAngleToPolarAngle();
double radian = angle * Math.PI / 180.0;
(double radiusX, double radiusY) = GetRadii();
var x = RenderSize.Width / 2.0 + radiusX * Math.Cos(radian);
var y = (RenderSize.Height - (StrokeThickness / 2.0)) - (radiusY + radiusY * Math.Sin(radian));
return new Point(x, y);
}
}
<Border Width="100" Height="100" Background="LightBlue">
<local:Arc Stroke="Brown" StrokeThickness="10" EndAngle="270" />
</Border>
加入邊框調整後的範例程式碼在此。
註:關於極座標的詳細內容可以參考 wiki 極座標系。