原文: WPF仿百度Echarts人口遷移圖 GitHub位址: https://github.com/ptddqr/wpf-echarts-map/tree/master 關于大名鼎鼎的百度Echarts我就不多說了 不了解的朋友直接看官方的例子吧 http://echarts.baidu.com/examples.html 效果圖:

關于可行性:以前常聽人說wpf動畫開多了會很卡,而我也沒有寫過含有大量動畫的項目,不知道實際怎樣,這個地圖顯然全是動畫,是以我寫了個測試動畫性能的小程式,生成100個點和線跑動畫,發現完全沒有什麼問題.
是以wpf做這個東西肯定是完全沒有問題的.附上這個小程式
動畫性能測試有興趣的朋友可以開點動畫 看看windows任務管理器裡的cpu和記憶體的消耗情況
先說下大體的思路吧:
- 如果你沒有搞設計的幫你做地圖的話,基本得去網上找矢量地圖,轉後轉換成path
- 找到省會城市的坐标,這就是運動軌迹的起點和終點
- 根據起點終點生成運動軌迹的path和跑動的點,在點上做路徑動畫,生成一個圓,中心放到到達城市的坐标處
- 初始化過程的動畫
布局
最初的最初,我們得先考慮布局,為了防止一旦做成使用者控件的話,設定尺寸時地圖走形.
- 最外層肯定要用Viewbox,按比例縮放.需要注意的是,Viewbox内部放的控件是必須有具體的尺寸的,它才能進行縮放,當然不一定必須要顯式的去設定内部的Width和Height,隻要内部有實際意義上的尺寸就行.
- Viewbox内先放一個Grid,分成兩列Width全部設定成auto,這樣能根據内部控件的實際大小來決定列寬.
- 0列放一個StackPanel.這個是左側當菜單用的RadioButton的容器,每個RadioButton都有具體的寬度,是以0列就有了具體的寬度
- 1列再放一個Grid,這個Grid一定要設定HorizontalAlignment="Left" VerticalAlignment="Top",就是靠左上角布局,這樣他内部的控件就會給它撐起來,也就有了具體的尺寸,這樣Viewbox才能夠縮放
- 把地圖的path全部放到這個Grid裡,path的Stretch必須是None,這樣path就會把這個Grid給撐起來,在這個Grid裡面所有path的下面再放一個Grid,用來做生成的動畫用的圖形的容器,他的坐标是和父級Grid的坐标重合的
地圖
關于找地圖,不好找,我沒有什麼好的心得.反正目的就是找一個帶有省會标記的地圖矢量圖,隻要是矢量圖,我們就應該有辦法把他轉換成Xmal.
我是在百度文庫裡找到一個ppt版的矢量地圖,0下載下傳券
矢量地圖素材下載下傳下來後用ppt打開,要用微軟的,别的可能儲存不了源檔案,右鍵地圖=>另存為=>選.emf格式,然後用Microsoft Expression Design打開,然後右鍵=>導出
這樣就得到了我們要的path,然後找到每個省會所對應的path,取他們的Canvas.Left+Width/2 Canvas.Top+Height/2 就是對應坐标點的(x,y).(我算的沒這麼精細,就是大概加了下.這個工作太枯燥,這不是重點.)
先吐槽下我找的這個地圖,北京和天津是連在一起的,廊坊也消失不見了,3個城市整個合成了一個path.是以建議大家自己再去找找
注意wpf的坐标都是以左上角開始的(0,0) 向右加x值 向下加y值 後面我們生成的圖形定位時都要 x值-自身Width/2 y值-自身Height/2 這樣才能讓圖形的中心對準需要定位的坐标點
有了地圖和坐标,我們就可以做下面的工作了
生成動畫所需要的跑動的點,運動軌迹的path,表示到達城市的圓圈
跑動的點
跑動的點,我用了一個Grid裡面套了一個path和一個Ellipse.
橢圓做陰影,顔色和軌迹一樣,加一個透明掩碼OpacityMask,裡面是一個放射型的漸變畫刷RadialGradientBrush.原點GradientOrigin(0.8,0.5) offset0處設定為不透明,offset1處不透明度設定為2/16.
水滴型的path我就用blend裡的鋼筆随意畫了一個,得到了它的Data. Fill給一個線型漸變畫刷,StartPoint(0,0),EndPoint(1,0),offset0給一個半透明的軌迹色,offset1給個不透明的純白.
這個Grid的IsHitTestVisible可以設定成false,不參與命中測試,這樣滑鼠在軌迹上時,點經過時,不會打斷軌迹ToolTip的顯示.
代碼控,想自己寫path的話,思路可以參考我的另一篇部落格
WPF繪制簡單常用的Path城市的圓
他就是個圓圈,沒什麼好說的,注意一下中心的定位就行了 Ellipse 顔色和軌迹一樣 ToolTip寫上你想顯示的東西
運動軌迹
我用的是弧線ArcSegment 兩個城市的點确定了,那麼可以通過兩個點的x,y,根據勾股定理計算出線段的長度.給一個點,連接配接這兩個城市的點,可以組成一個三角形,兩個城市組成的線段對面的那個角可以設定成一個角度參數,
這個線段固定,對角的角度固定,那麼他所對應的外接圓的圓弧就是固定的.我們可以根據正弦定理a/sinA=2r求出外接圓的半徑.就可以畫出這個弧線來了.然後可以給這個path的ToolTip附上滑鼠移上去想顯示的文字.改下ToolTip的樣式就行了
動畫
點沿着軌迹跑的動畫
這部分動畫,我就不說了,參考周銀輝的部落格
http://www.cnblogs.com/zhouyinhui/archive/2007/07/31/837893.html城市的圓的動畫
給Ellipse的透明掩碼OpacityMask加一個放射型的漸變畫刷RadialGradientBrush,加三個節點,offset0,offset1都是不透明的,在他們中間加一個完全透明的節點,然後動畫控制offset值由0到1或由1到0,效果不同.
這部分動畫其實就是計算時間,在合适的時間開始合适的動畫.
運動軌迹的呈現:就是給運動軌迹path的透明掩碼給一個線型漸變畫刷,根據向左,右,上,下運動,設定好StartPoint和EndPoint,然後兩個節點一個透明,一個不透明,同時從0向1做動畫,需要注意的是如果一前一後運動,一定要透明的那個節點在前面運動,
不然會出現很怪異的行為,把這個動畫的時間設定成跑動的點的一半的時間.這樣軌迹比點跑的快,不至于點跑過去了,路徑還沒有呈現到那
關于城市的圓,這部分加的比較多,首先可以用一個DoubleAnimation來控制Ellipse的透明度,開始時間是軌迹呈現的時間,也就是點的時間/2,這樣剛好軌迹呈現到圓時,圓開始呈現,動畫時間也設定成軌迹呈現時間,這樣剛好點運動到圓的時候,圓已經完全呈現完.
然後加一個ColorAnimation,來控制圓透明掩碼裡放射畫刷的第二個節點,也就是控制點,讓他變為透明,用時0就可以,這樣就可以繼續圓的放射型動畫了.開始時間就是點運動到圓的時間.
接下來就是一些RadioButton,ToolTip,Path的樣式問題了.這部分大家看心情,做個自己喜歡的樣式就可以了.
2016-08-01更新:
将名稱注冊動畫改為對象注冊動畫
1 private void AddPointToStoryboard(Grid runPoint, Ellipse toEll, Storyboard sb, Path particlePath, double l, ProvincialCapital from, MapToItem toItem)
2 {
3 double pointTime = l / m_Speed;//點運動所需的時間
4 double particleTime = pointTime / 2;//軌迹呈現所需時間(跑的比點快兩倍)
5 ////生成為控件注冊名稱的guid
6 //string name = Guid.NewGuid().ToString().Replace("-", "");
7
8 #region 運動的點
9 TransformGroup tfg = new TransformGroup();
10 MatrixTransform mtf = new MatrixTransform();
11 tfg.Children.Add(mtf);
12 TranslateTransform ttf = new TranslateTransform(-runPoint.Width / 2, -runPoint.Height / 2);//糾正最上角沿path運動到中心沿path運動
13 tfg.Children.Add(ttf);
14 runPoint.RenderTransform = tfg;
15 //this.RegisterName("m" + name, mtf);
16
17 MatrixAnimationUsingPath maup = new MatrixAnimationUsingPath();
18 maup.PathGeometry = particlePath.Data.GetFlattenedPathGeometry();
19 maup.Duration = new Duration(TimeSpan.FromSeconds(pointTime));
20 maup.RepeatBehavior = RepeatBehavior.Forever;
21 maup.AutoReverse = false;
22 maup.IsOffsetCumulative = false;
23 maup.DoesRotateWithTangent = true;
24 //Storyboard.SetTargetName(maup, "m" + name);
25 //Storyboard.SetTargetProperty(maup, new PropertyPath(MatrixTransform.MatrixProperty));
26 Storyboard.SetTarget(maup, runPoint);
27 Storyboard.SetTargetProperty(maup, new PropertyPath("(Grid.RenderTransform).Children[0].(MatrixTransform.Matrix)"));
28 sb.Children.Add(maup);
29 #endregion
30
31 #region 達到城市的圓
32 //this.RegisterName("ell" + name, toEll);
33 //軌迹到達圓時 圓呈現
34 DoubleAnimation ellda = new DoubleAnimation();
35 ellda.From = 0.2;//此處值設定0-1會有不同的呈現效果
36 ellda.To = 1;
37 ellda.Duration = new Duration(TimeSpan.FromSeconds(particleTime));
38 ellda.BeginTime = TimeSpan.FromSeconds(particleTime);//推遲動畫開始時間 等軌迹連接配接到圓時 開始播放圓的呈現動畫
39 ellda.FillBehavior = FillBehavior.HoldEnd;
40 //Storyboard.SetTargetName(ellda, "ell" + name);
41 //Storyboard.SetTargetProperty(ellda, new PropertyPath(Ellipse.OpacityProperty));
42 Storyboard.SetTarget(ellda, toEll);
43 Storyboard.SetTargetProperty(ellda, new PropertyPath(Ellipse.OpacityProperty));
44 sb.Children.Add(ellda);
45 //圓呈放射狀
46 RadialGradientBrush rgBrush = new RadialGradientBrush();
47 GradientStop gStop0 = new GradientStop(Color.FromArgb(255, 0, 0, 0), 0);
48 //此為控制點 color的a值設為0 off值走0-1 透明部分向外放射 初始設為255是為了初始化效果 開始不呈放射狀 等跑動的點運動到城市的圓後 color的a值才設為0開始呈現放射動畫
49 GradientStop gStopT = new GradientStop(Color.FromArgb(255, 0, 0, 0), 0);
50 GradientStop gStop1 = new GradientStop(Color.FromArgb(255, 0, 0, 0), 1);
51 rgBrush.GradientStops.Add(gStop0);
52 rgBrush.GradientStops.Add(gStopT);
53 rgBrush.GradientStops.Add(gStop1);
54 toEll.OpacityMask = rgBrush;
55 //this.RegisterName("e" + name, gStopT);
56 //跑動的點達到城市的圓時 控制點由不透明變為透明 color的a值設為0 動畫時間為0
57 ColorAnimation ca = new ColorAnimation();
58 ca.To = Color.FromArgb(0, 0, 0, 0);
59 ca.Duration = new Duration(TimeSpan.FromSeconds(0));
60 ca.BeginTime = TimeSpan.FromSeconds(pointTime);
61 ca.FillBehavior = FillBehavior.HoldEnd;
62 //Storyboard.SetTargetName(ca, "e" + name);
63 //Storyboard.SetTargetProperty(ca, new PropertyPath(GradientStop.ColorProperty));
64 Storyboard.SetTarget(ca, toEll);
65 Storyboard.SetTargetProperty(ca, new PropertyPath("(Ellipse.OpacityMask).(GradientBrush.GradientStops)[1].(GradientStop.Color)"));
66 sb.Children.Add(ca);
67 //點達到城市的圓時 呈現放射狀動畫 控制點的off值走0-1 透明部分向外放射
68 DoubleAnimation eda = new DoubleAnimation();
69 eda.To = 1;
70 eda.Duration = new Duration(TimeSpan.FromSeconds(2));
71 eda.RepeatBehavior = RepeatBehavior.Forever;
72 eda.BeginTime = TimeSpan.FromSeconds(particleTime);
73 //Storyboard.SetTargetName(eda, "e" + name);
74 //Storyboard.SetTargetProperty(eda, new PropertyPath(GradientStop.OffsetProperty));
75 Storyboard.SetTarget(eda, toEll);
76 Storyboard.SetTargetProperty(eda, new PropertyPath("(Ellipse.OpacityMask).(GradientBrush.GradientStops)[1].(GradientStop.Offset)"));
77 sb.Children.Add(eda);
78 #endregion
79
80 #region 運動軌迹
81 //找到漸變的起點和終點
82 Point startPoint = GetProvincialCapitalPoint(from);
83 Point endPoint = GetProvincialCapitalPoint(toItem.To);
84 Point start = new Point(0, 0);
85 Point end = new Point(1, 1);
86 if (startPoint.X > endPoint.X)
87 {
88 start.X = 1;
89 end.X = 0;
90 }
91 if (startPoint.Y > endPoint.Y)
92 {
93 start.Y = 1;
94 end.Y = 0;
95 }
96 LinearGradientBrush lgBrush = new LinearGradientBrush();
97 lgBrush.StartPoint = start;
98 lgBrush.EndPoint = end;
99 GradientStop lgStop0 = new GradientStop(Color.FromArgb(255, 0, 0, 0), 0);
100 GradientStop lgStop1 = new GradientStop(Color.FromArgb(0, 0, 0, 0), 0);
101 lgBrush.GradientStops.Add(lgStop0);
102 lgBrush.GradientStops.Add(lgStop1);
103 particlePath.OpacityMask = lgBrush;
104 //this.RegisterName("p0" + name, lgStop0);
105 //this.RegisterName("p1" + name, lgStop1);
106 //運動軌迹呈現
107 DoubleAnimation pda0 = new DoubleAnimation();
108 pda0.To = 1;
109 pda0.Duration = new Duration(TimeSpan.FromSeconds(particleTime));
110 pda0.FillBehavior = FillBehavior.HoldEnd;
111 //Storyboard.SetTargetName(pda0, "p0" + name);
112 //Storyboard.SetTargetProperty(pda0, new PropertyPath(GradientStop.OffsetProperty));
113 Storyboard.SetTarget(pda0, particlePath);
114 Storyboard.SetTargetProperty(pda0, new PropertyPath("(Path.OpacityMask).(GradientBrush.GradientStops)[0].(GradientStop.Offset)"));
115 sb.Children.Add(pda0);
116 DoubleAnimation pda1 = new DoubleAnimation();
117 //pda1.From = 0.5; //此處解開注釋 值設為0-1 會有不同的軌迹呈現效果
118 pda1.To = 1;
119 pda1.Duration = new Duration(TimeSpan.FromSeconds(particleTime));
120 pda1.FillBehavior = FillBehavior.HoldEnd;
121 //Storyboard.SetTargetName(pda1, "p1" + name);
122 //Storyboard.SetTargetProperty(pda1, new PropertyPath(GradientStop.OffsetProperty));
123 Storyboard.SetTarget(pda1, particlePath);
124 Storyboard.SetTargetProperty(pda1, new PropertyPath("(Path.OpacityMask).(GradientBrush.GradientStops)[1].(GradientStop.Offset)"));
125 sb.Children.Add(pda1);
126 #endregion
127 }
View Code
2016-12-19更新:
釋出到GitHub,位址:
源碼下載下傳:
仿百度Echarts人口遷移圖.zip