【问题标题】:How to position labels around circle?如何在圆圈周围放置标签?
【发布时间】:2016-03-29 03:53:43
【问题描述】:

我正在尝试弄清楚如何计算文本标签围绕圆圈的位置。这比您第一次阅读  时可能想象的要复杂一些。

我了解了基础知识:

X = ptCenter.X + (dRadius * Math.Cos(dAngle * Math.PI / 180.0))
Y = ptCenter.Y + (dRadius * Math.Sin(dAngle * Math.PI / 180.0))

所以,这将给我圆上的点,角度为 dAngle,半径为 dRadius。当然,DrawText(任何种类,但如果这对您的答案有任何影响,我会专门使用 DrawingContext.DrawText)将给定点作为文本的左上角进行绘制。

问题是,这不是绘制文本的正确位置。以下是该问题的说明:

https://support.office.com/en-us/article/Present-your-data-in-a-radar-chart-16e20279-eed4-43c2-9bf5-29ff9b10601d

Jan 以该点为中心水平居中 二月是从左下角绘制的 Mar 似乎在水平的中间 Apr 垂直居中 等等。

标签没有以统一的方式围绕点定位。这有点取决于你的绘图角度。

我需要绘制的标签数量可能会有所不同,因此硬编码软糖因素已不存在。我需要绘制的角度也可以变化,所以那里也没有软糖因素。所有这些都必须即时计算。

有点像 0、90、180、270 是特殊情况,而其他似乎是围绕该点垂直半居中,但根据您所谈论的圆的哪一侧向右或向左绘制?

我在正确的轨道上吗?还是有“已知”的算法?

谢谢。

【问题讨论】:

  • 显然您将不得不计算文本的中心点,这当然会因文本而异。Here 是计算相对于文本本身的事物的示例。看起来FormattedText 类将为您提供所需的数据(宽度、WidthIncludingTrailingWhitespace 等。)

标签: c# wpf


【解决方案1】:

你是对的,问题并不像看起来那么简单。用铅笔和纸坐下来后,我认为问题可以分解为更简单的步骤:

  1. 确定圆的尺寸和所需的边距(图片上:蓝色虚线圆圈)。
  2. 确定标签文本的大小(在图像上:标签周围的黑框)。
  3. 确定(黑色)标签文本框架上应该接触蓝色虚线圆圈的点。对于每个维度 (X, Y) 分别:
    • 如果文本标签的中心点在距离轴的标签大小的一半以内(在图像上:在橙色双线内 - 请参阅圆圈上方的标签),那么您的点在 在边缘标签文本框
    • 否则,该点在标签文本框的角落
  4. 将标签从圆圈的中间“移动”,使其黑框在您在步骤3中确定的点接触蓝色虚线。 (上图:从圆心到标签中心的红线)
  5. 根据与圆接触的点,确定标签的左上角并在结果位置绘制标签。

抱歉,这里没有公式,我需要更多时间来编写它们。但我想告诉你,在这里定位标签需要的不仅仅是用两行代码对sin()cos() 进行简单的评估。

【讨论】:

  • 很好的回应。不过,我认为问题更简单。您需要关注 8 个可能的点:标签边界矩形的角和线的中心。根据围绕圆的角度计算盒子的位置,使对应的点在圆上。例如,对于角度 0,您关心左侧点,对于角度 PI/4,您关心左上点,对于 PI/2,您考虑顶部。
  • @SideriteZackwehdex - 你有超过 8 个点:如果标签靠近轴,则每条边上有 4 个角和无限个点(不仅仅是 4 个)。想象一个标签正好位于轴的中心并开始移动它。在标签停止与轴相交之前,接触点不会是角(如果可视化应该看起来不错)。
  • 除非你想真正为围绕圆圈旋转的标签设置动画,否则我会接受近似值,尤其是边距,正如你所描述的那样。事实上,如果您在切换象限时对“跳跃”标签感到满意,您可能只需要角落。当然,某处肯定有数学公式,但真的有必要吗?
  • @SideriteZackwehdex - 这不是关于动画,而是关于标签的均匀分布。根据您的建议,圆圈周围有 5 个标签不会发现差异,但如果您有 20 个标签,可能会发现差异,其中一些标签落在轴附近。我不认为有两个不同的计算案例会以任何方式困难或耗时。只需多坐几分钟来创建适当的公式。 :)
  • 好的,我不想花太多时间在这上面,但是考虑一下:在 8 点方法中,您所要做的就是使用四个象限的角,除非当象限分隔符的角度足够小,可以在所需距离处包含一半大小的文本。例如,对于接近 0 的角度,您将拥有 -/+atan2(height/2,radius)
【解决方案2】:

您需要将文本中心定位在您计算的点上。因此,您需要将点在 X 方向上移动半个宽度,在 Y 方向上移动半个高度。这会将标签定位在圆的“内部”。像这样:

public class CircleText : FrameworkElement {
    public string[] Labels
    {
        get { return (string[])GetValue(LabelsProperty); }
        set { SetValue(LabelsProperty, value); }
    }

    public static readonly DependencyProperty LabelsProperty =
        DependencyProperty.Register("Labels", typeof(string[]), typeof(CircleText), new PropertyMetadata(null, OnLabelsChanged));

    private static void OnLabelsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
        ((CircleText) d).InvalidateVisual();
    }

    protected override void OnRender(DrawingContext drawingContext) {
        if (Labels == null || Labels.Length == 0)
            return;
        var centerX = this.ActualWidth / 2;
        var centerY = this.ActualHeight / 2;
        var rad = Math.Min(this.ActualWidth / 2, this.ActualHeight / 2);
        for (int i = 0; i < Labels.Length; i++) {
            var angle = 360 / (Labels.Length) * i;
            var x = centerX + rad * Math.Cos(angle * Math.PI / 180.0);
            var y = centerY + rad * Math.Sin(angle * Math.PI / 180.0);
            FormattedText text = new FormattedText(
                Labels[i],
                CultureInfo.GetCultureInfo("en-us"),
                FlowDirection.LeftToRight,
                new Typeface("Verdana"),
                12,
                Brushes.Black);
            x -= text.Width / 2;
            y -= text.Height / 2;
            drawingContext.DrawText(text, new Point(x, y));
        }

    }
}

如果您想在点上画线并且希望标签位于这些线之外 - 您需要根据已计算的 cos 和 sin 值移动标签。这会将标签定位在“外部”,如下所示:

protected override void OnRender(DrawingContext drawingContext) {
        if (Labels == null || Labels.Length == 0)
            return;
        var centerX = this.ActualWidth / 2;
        var centerY = this.ActualHeight / 2;
        var rad = Math.Min(this.ActualWidth / 2, this.ActualHeight / 2);
        for (int i = 0; i < Labels.Length; i++) {
            var angle = 360 / (Labels.Length) * i;
            var xshift = Math.Cos(angle * Math.PI / 180.0);
            var yshift = Math.Sin(angle * Math.PI / 180.0);
            var x = centerX + rad * xshift;
            var y = centerY + rad * yshift;
            drawingContext.DrawLine(new Pen(Brushes.Black, 1), new Point(centerX, centerY), new Point(x,y));
            FormattedText text = new FormattedText(
                Labels[i],
                CultureInfo.GetCultureInfo("en-us"),
                FlowDirection.LeftToRight,
                new Typeface("Verdana"),
                12,
                Brushes.Black);
            x -= (1 - xshift) * text.Width / 2;
            y -= (1 - yshift) * text.Height / 2;                
            drawingContext.DrawText(text, new Point(x, y));
        }            
    }

当然,上述方法适用于任意数量的标签。

【讨论】:

  • 谢谢!我已经用你的示例代码玩了几分钟(开会哈哈),但它似乎把标签放在了正确的位置!投票赞成并标记为答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-06-26
  • 1970-01-01
  • 2021-06-29
  • 2017-07-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多