QStockMapDrawing 简单的股票图绘制

这是啥

参考东方财富交互 的WPF 股票图绘制控件,整体围绕着 DrawingVisual 实现的,K线显示比较麻烦 还没做完,但分时图和基础功能完成 整体还算精致。

不过发现一个问题 那就是绘制的效率可能随着 窗体变大 出现卡顿问题,所以后续可能要用别的代替DrawingVisual

整了好几次了 每次做都算的抓头皮,也不知道这叫什么

分时

image

K线

image

基类

我定义了一个 StockMapBase作为基类,继承了FrameworkElement

重写一下子项的逻辑,因为想要从这里开始创建绘制层

      private UIElementCollection Childs = null;
	  ...
      protected override Visual GetVisualChild(int index)
      {
          return Childs[index];
      }
      protected override int VisualChildrenCount => Childs.Count;

让基类提供一个绘制层的创建方式,之后的绘制就要以这为中心创建了。

        /// <summary>
        /// 获取或创建一个绘制对象
        /// </summary>
        /// <param name="Key">键值</param>
        /// <param name="Index">如果是创建插入到第几个,默认为null。null则代表通过ADD添加</param>
        /// <returns></returns>
        protected DrawingVisualHost GetOrCreateDrawingVisualView(string Key, int? Index = null)
        {
            var target = GetDrawingVisualView(Key);
            if (target == null)
            {
                target = CreateDrawingVisualView(Key, Index);
            }
            return target;
        }

然后这里应该要有一个初始化 ”图层“ 的步骤,往后的子类也将通过这个方法去初始化,二上面提供的 插入 方式也就是为了不同时初始化时机想要不同的层级准备被的。

    /// <summary>
    /// 初始化 绘制层 0=背景层 1=鼠标移动参考线层
    /// </summary>
    protected virtual void InitDrawingVisualViews()
    {
        BackgroundDrawingTarget = GetOrCreateDrawingVisualView("DRAW_BACKGROUND");
        MouseMoveDrawingView = GetOrCreateDrawingVisualView("DRAW_StockMouseMoveXYLine");
        MouseMoveDrawingView.IsHitTestVisible = false;
    }

这所谓的基类还应该提供一个绘制区域大小,因为有些时候是要让绘制区域变大点变小点的,所以这里最好别 写死 ,而且因为是WPF 所以提供一个Margin的依赖属性是个不错的选择。

    /// <summary>
    /// 获取绘制区大小
    /// </summary>
    protected virtual Rect GetDrawingRect()
    {
        if (!IsDrawingRectTempChanged && DrawingRectTemp != null) return DrawingRectTemp.Value;
        Thickness DefaultMargin = new(0);
        Thickness dm = DefaultMargin, nm = DrawingMargin;
        var mg = new Thickness(dm.Left + nm.Left, dm.Top + nm.Top, dm.Right + nm.Right, dm.Bottom + nm.Bottom);
        var rs = RenderSize;
        var dr1 = (DrawingRectTemp = new()
        {
            X = mg.Left,
            Y = mg.Top,
            Width = Math.Max(rs.Width - mg.Left - mg.Right, 1),
            Height = Math.Max(rs.Height - mg.Top - mg.Bottom, 1)
        }).Value;
        return dr1;
    }

然后就是鼠标的参考线绘制逻辑问题,因为很多图都需要 所以干脆就在基类里声明一下绘制,先别管绘制的位置数据 只需要知道位置就行。而且后续可能会重写MouseMove事件,所以干脆再以中间声明一个Core方便子类抛弃这个设置逻辑。

    /// <summary>
    /// 这里是鼠标移动的基础逻辑,方便重写
    /// </summary>
    protected virtual void OnMouseMoveMapCore(MouseEventArgs e)
    {
        Point? mp = e.GetPosition(this);
        if (this.IsShowMouseMoveXYLine)
        {
            SetMouseXYLinePoint(mp.Value);
        }
        else
        {
            mp = null;
            SetMouseXYLinePoint(null);
        }
    }

分时图

功能 完成度 注释
价格线 完成
均线 完成
价格&百分比 完成 这里还蛮坑的
成交量 完成
鼠标参考线 完成 交给用户空间
成交量&分时图 参考线联动 完成 难点在于绝对和相对定位转换
参考线信息 一半 详细信息还没做,目前是参考线右面做了
导航选择 完成 就是方向键选择分时,这里要适配鼠标移动选择
一次性数据 完成 数据传输接口中定义
流传输 完成 还是那个接口 它还定义了流方式
多日数据 没实现 这个有点难,因为日期和年月日又挂钩了 就没做

价格映射

分时图这边最核心的问题就 位置和价格+事件的映射问题,所以实现这俩方法,其实这个分时图就完成一半了。

注意 max 和 min 不是写死的,因为它们可能随着股价变化而变动,所以最好写成方法。具体实现方式就得看股票分时图的绘制方案,比如什么时候绘制涨停价?什么时候绘制5% 什么时候3%这种。

然后就是一个 **午盘休息 **那段时间的问题,所以在这里判断逻辑就改成了<=50%视为上午到中午 >50%就当成下午,这样就像写死了

    /// <summary>
    /// 获取某个时间、价格对应的坐标位置
    /// </summary>
    protected Point GetPricePoint(DateTime Time, decimal Price)
    {
        var TimeSp = Time.TimeOfDay;
        var max = GetMapMaxPrice();
        var min = GetMapMinPrice();
        var size = GetDrawingRect();
        double X = 0, Y = 0;
        var XP = StaticStockTimeProvider.GetTimePersentage(TimeSp);
        X = XP * size.Width;
        double YP = (double)((Price - min) / (max - min));
        Y = size.Height * (1 - YP);
        return new(X + size.Left, Y + size.Top);
    }
    /// <summary>
    /// 获取某个位置的时间、价格
    /// </summary>
    protected (TimeSpan, decimal) GetPointPrice(Point Pos)
    {
        var size = GetDrawingRect();
        var relativePoint = new Point(Pos.X - size.Left, Pos.Y - size.Top);
        var persentage = relativePoint.X / size.Width;
        var time = StaticStockTimeProvider.GetPersentageTime(persentage);
        decimal price = 0;
        var yp = 1 - (relativePoint.Y / size.Height);
        var max = GetMapMaxPrice();
        var min = GetMapMinPrice();
        price = (decimal)yp * (max - min) + min;
        return (time, price);
    }

绘制样式问题

这是获取整个图关系的核心

他俩都会通过获取目前显示样式从而动态变化

如果是最大值 那就是根据 开盘价 来算,不然就可能出现零轴线消失 变成一大半都是绿色或红色,其他也同理 就是想怎么画就在这里实现。

/// <summary>
/// 获取分时图的最高价格,根据当前价格范围自动调整,保证分时图的可视化效果
/// </summary>
private decimal GetMapMaxPrice()
{
    var style = GetMapRangeStyle(out var zpMax, out var zpMin, out var zpBig);
    return style == 1 ? MinuteStockSource.ZeroPrice * (1 + zpBig) : style == 2 ? MinuteStockSource.ZeroPrice * (decimal)1.05 : style == 3 ? GetFullPrice() : throw new("未声明的绘制价格方式Style " + style);

}
/// <summary>
/// 获取分时图的最小价格,根据当前价格范围自动调整,保证分时图的可视化效果
/// </summary>
private decimal GetMapMinPrice()
{
    var style = GetMapRangeStyle(out var zpMax, out var zpMin, out var zpBig);
    return style == 1 ? MinuteStockSource.ZeroPrice * (1 - zpBig) : style == 2 ? MinuteStockSource.ZeroPrice * (decimal)0.95 : style == 3 ? GetDownPrice() : throw new("未声明的绘制价格方式Style " + style);
}

样式

目前就是三个样式

可以做成Enum但感觉无所谓了

 /// <summary>
 /// 目前绘制的方式
 /// </summary>
 /// <returns>1=最大值绘制,2=涨停一半的方式,3=涨停绘制</returns>
 private int GetMapRangeStyle(out decimal zpMax, out decimal zpMin, out decimal zpLastBig)
 {
     var max = MaxStockPrice;
     var min = MinStockPrice;
     var zero = MinuteStockSource.ZeroPrice;
     zpMax = (max - zero) / zero;
     zpMin = (zero - min) / zero;
     zpLastBig = Math.Max(zpMax, zpMin);
     if (zpLastBig > (decimal)0.05)
     {
         return 3;
     }
     else if (zpLastBig > (decimal)0.01)
     {
         return 2;
     }
     else
     {
         return 1;
     }
 }

选择

最好是提供一个选择类,不然很乱很乱 放在一块耦合度太高了。

难点在于和基类那个设置鼠标位置的方法,联动 互相设置问题。

image

K线图

这玩意和分时有点不一样,不是绘制 是数据层面

因为它动不动就好几千个 所以需要一些算法去优化查找时间,不然一个小区间数据卡一下绘制就会很糟糕。

目前也是刚开始做,所以也不知道对不对。

功能 完成度 注释
K线显示 完成 最基本的 上影线啥的
参考线 一半 这是基类提供的
虚线&价格 完成 右面那个线标价格
方向键区间变换 感觉是有点费劲,尤其是变大区间
其他 感觉还有好多坑要填,但暂时想不出来了

...

找对应日期的K线

想要找一个可能存在也可能不存在的日期对应的K线就很难,因为字典能存储绝对定位的 日期K,但差不多那块怎么处理?所以这块的自己想着就是每次找都分十份,然后看自己要的那个日期在哪里,最后查到的如果不是这个日期 不管是不是都返回,这就是差不多。

找的差不多了 我这是剩下一百个K线数据就开始地毯搜索,这都找不到那就是那天应该休息了。

            /// <summary>
            /// 找日期最近的那个K线
            /// </summary>
            public StockKLineModel? FindDateLikeItem(DateTime Date)
            {

                List<StockKLineModel> datas = new();
                datas.AddRange(this.KLines);

                while (true)
                {
                    var findresult = FindTestRange(datas);
                    datas = findresult;
                    if (datas.Count == 0)
                    {
                        return null;
                    }
                    else if (datas.Count <= 100)
                    {
                        if (datas.Count == 1)
                        {
                            return findresult.First();
                        }
                        else
                        {
                            StockKLineModel last = null;
                            foreach (var i in datas)
                            {
                                last = i;
                                if (i.Date >= Date)
                                {
                                    break;
                                }
                            }
                            return last;
                        }
                    }
                }
                return null;
                List<StockKLineModel> FindTestRange(List<StockKLineModel> source)
                {
                    int step = source.Count / 10;
                    for (int i = 0; i < source.Count; i += step)
                    {
                        var start = i;
                        var end = (i + step) > source.Count - 1 ? source.Count - 1 : (i + step);
                        StockKLineModel s = source[start], e = source[end];
                        if (s.Date == Date) return new() { s };
                        else if (e.Date == Date) return new() { e };
                        if (s.Date < Date && e.Date > Date)
                        {
                            List<StockKLineModel> ls = new();
                            for (int tk = start; tk <= end; tk++)
                            {
                                ls.Add(source[tk]);
                            }
                            return ls;
                        }
                    }
                    return new();
                }
            }

有意思的地方好像就这些了,遇到再发。

声明: 本文仅为 WPF 绘图、坐标计算、数据可视化 相关的技术学习与经验分享,所有内容仅限技术研究使用。 文中涉及的界面风格仅为学习参考,未完整实现任何第三方软件功能,不提供完整项目源码,不构成任何投资建议。

posted @ 2026-03-05 12:48  quxingbai  阅读(10)  评论(0)    收藏  举报