可視化工具solo show-----Prefuse自帶例子GraphView講解


  2014.10.15日以來的一個月,擠破了頭、跑斷了腿、傷透了心、吃夠了全國最大餐飲連鎖店——沙縣小吃。其中酸甜苦辣,絕不是三言兩語能夠說得清道的明的。校招的兄弟姐妹們,你們懂得……

  體會最深的一句話還是:出來混,遲早要還的。

  一個月過去了,迷茫之際有太多無奈,無奈中又夾雜些許慶幸,歲月匆匆,人生不息,奮斗不止。

  遵守最初的諾言,繼續走我可視化的道路:

  上集摘要:一個月博文中大概介紹了可視化的一些常用工具,從可操作性、實用性、交互性等各方面進行了簡單的對比和總結,具體參見http://www.cnblogs.com/bigdataZJ/p/VisualizationSoloShow.html,結合自己的需求,挑出了Prefuse和Processing兩員大將出來露了一手,詳情請見http://www.cnblogs.com/bigdataZJ/p/VisualizationSoloShow2.html

  一番角逐之后,Prefuse工具集脫穎而出,其強大的展示效果、開發者友好的API說明文檔、豐富的自帶Demo無一不讓我對其欲罷不能。下面我們來好好分析下Prefuse的強大之處:

1.Prefuse主要特征:

  (1)任意數據類型的表格、圖和樹形數據結構,數據索引、選擇查詢,有效的內存占用

  (2)具有布局、着色、大小、圖形編碼、扭曲、動畫等多個組件

  (3)具有交互控制庫

  (4)支持動畫過渡,通過一系列的活動調度機制

  (5)支持平移、縮放等視圖變換

  (6)對於交互過濾數據的動態查詢

  (7)能夠使用可用的搜索引擎進行文本檢索

  (8)具有布局和動畫的力導向模擬引擎

  (9)靈活的多視圖展現,包括“概述+細節”和“小倍數”顯示

  (10)內置類SQL語句查詢,可以用於編寫查詢語句實現查詢指定字段的數據

  (11)提供查詢語句到Prefuse數據結構的數據映射的SQL查詢

  (12)簡單、開發者友好的APIs文檔

2.Prefuse模型:

  (1)prefuse.data包提供了 Table, Graph, Tree等數據結構;提供了一個data tables,他的行使用一個類 Tuple來表示;這個包中,Node和Edge來表示圖或者樹的一些成員。
  作為一種高級特征的工具集,Prefuse提供了一種解釋性的表達式語言,該語言可以用來請求Prefuse中的數據結構並根據已有的數據列創建衍生的列數據。表達式語言的功能實現類在prefuse.data.expression包中,文本表達式解析類在ExpressionParser類中。
  (2)prefuse.data.io包提供了文件的讀寫,包括表,圖和樹的結構,其中,表的格式:CSV和任意分割的文本文件,對於網絡,有 GraphML和 TreeML(XML也能);prefuse.data.io.sql包提供了對SQL數據庫的查詢,並返回一個prefuse表
  (3)可視化抽象是通過將數據添加到Visulization實例中來得到的,它除了包含原始數據外,還建立了一套完整的可視化體系,包括x、y的坐標軸,顏色,大小字體等值,任意的Tuple, Node, 或者 Edge被添加到Visulization實例中時候,相關的VisualItems實例就建立好了,如NodeItem和 EdgeItem就是VisualItems的實例。(也就是說,可視化抽象實現了添加的數據元素與VisualItems之間的映射)
  (4)可視化映射工作由Action模塊來完成,它是有一系列獨立的處理模塊組成的,這些模塊來完成可視性、布局計算、顏色設定以及任何其他的可視化工作。prefuse.action包以及其子包會提供一系列布局,形變,動畫以及可視化編碼的工作。
  (5)Renderer模塊決定了VisualItems的出現情況,Renderers模塊負責計算顯示區域,即如何將可視化圖形繪制在屏幕上。RendererFactory用來對Renderer進行管理,體現在給VisualItems分配適當的Renderer上。
  (6)交互工作,Display組建負責完成交互方面的工作,起到一個類似於攝像機的功能,對顯示的區域進行選取,縮放。它直接與用戶相關。
一個Visualization可以與多個Display實例關聯,以實現多視圖參數配置,比如“概述+詳細”以及小倍數顯示視圖等。
  (7)每個Display實例都支持若干個Controls,他們負責處理Display上鼠標和鍵盤的action。prefuse.controls包提供了一個預處理的控制器可以用來完成旋轉縮放Display的工作,通過prefuse.controls包的子類ControlAdapter可以實現對Display的控制。
  (8)最后,prefuse.data.query 包提供了動態查詢綁定(?)的功能,這些綁定能夠生成合適的用戶界面組建,來直接操作這些查詢。

 

 3.Prefuse自帶Demo---GraphView.java詳解

  下面是自己在研讀Prefuse源碼文件夾demos下的GraphView加的一些注釋:

  1 //start of class GraphView
  2 
  3 public class GraphView extends JPanel {
  4 
  5     private static final String graph = "graph";
  6 
  7     private static final String nodes = "graph.nodes";
  8 
  9     private static final String edges = "graph.edges";
 10 
 11     private Visualization m_vis;
 12 
 13    
 14 
 15     public GraphView(Graph g, String label) {
 16 
 17          super(new BorderLayout());//在GraphView的構造函數中調用超類的構造方法,並創建布局BorderLayout對象。
 18 
 19         // create a new, empty visualization for our data
 20 
 21         m_vis = new Visualization();//創建Visualization對象,使用默認的渲染工廠(DefaultRendererFactory)。Visualization類負責管理源數據與可視化組件之間的映射。
 22 
 23         // --------------------------------------------------------------------
 24 
 25         // set up the renderers
 26 
 27         LabelRenderer tr = new LabelRenderer();
 28 
 29         tr.setRoundedCorner(8, 8);
 30 
 31         m_vis.setRendererFactory(new efaultRendererFactory(tr));//新建標簽渲染器並注冊到Visualization上,使用的還是DefaultRendererFactory。
 32 
 33         // --------------------------------------------------------------------
 34         // register the data with a visualization
 35 
 36         // adds graph to visualization and sets renderer label field
 37 
 38         setGraph(g, label);// 向Visualization添加圖形Graph並為標簽域賦值。
 39 
 40        
 41 
 42         // fix selected focus nodes   聲明一個數據元組集合,並為該集合添加一個數據元組的監聽器
 43 
 44         TupleSet focusGroup = m_vis.getGroup(Visualization.FOCUS_ITEMS);
 45 
 46         focusGroup.addTupleSetListener(new TupleSetListener() {
 47 
 48         public void tupleSetChanged(TupleSet ts, Tuple[] add, Tuple[] rem)
 49 
 50             {
 51 
 52                 for ( int i=0; i<rem.length; ++i )
 53 
 54                     ((VisualItem)rem[i]).setFixed(false);
 55 
 56                 for ( int i=0; i<add.length; ++i ) {
 57 
 58                     ((VisualItem)add[i]).setFixed(false);
 59 
 60                     ((VisualItem)add[i]).setFixed(true);
 61 
 62                 }
 63 
 64                 if ( ts.getTupleCount() == 0 ) {
 65 
 66                     ts.addTuple(rem[0]);
 67 
 68                     ((VisualItem)rem[0]).setFixed(false);
 69 
 70                 }
 71 
 72                 m_vis.run("draw");
 73 
 74             }
 75 
 76         });//聲明一個數據元組集合,並通過匿名內部類的形式為該集合添加一個數據元組的監聽器(TupleSetListener),其中ts:變化的數據元組;add:已經加入的元組數組集合;rem:移除的數據集合。
 77 
 78          // --------------------------------------------------------------------
 79 
 80         // create actions to process the visual data
 81 
 82  
 83 
 84         int hops = 30;
 85 
 86         final GraphDistanceFilter filter = new GraphDistanceFilter(graph, hops);
 87 
 88  
 89 
 90         ColorAction fill = new ColorAction(nodes,
 91 
 92                 VisualItem.FILLCOLOR, ColorLib.rgb(200,200,255));
 93 
 94         fill.add(VisualItem.FIXED, ColorLib.rgb(255,100,100));
 95 
 96         fill.add(VisualItem.HIGHLIGHT, ColorLib.rgb(255,200,125));
 97 
 98        
 99 
100         ActionList draw = new ActionList();
101 
102         draw.add(filter);
103 
104         draw.add(fill);
105 
106         draw.add(new ColorAction(nodes, VisualItem.STROKECOLOR, 0));
107 
108         draw.add(new ColorAction(nodes, VisualItem.TEXTCOLOR, ColorLib.rgb(0,0,0)));
109 
110         draw.add(new ColorAction(edges, VisualItem.FILLCOLOR, ColorLib.gray(200)));
111 
112         draw.add(new ColorAction(edges, VisualItem.STROKECOLOR, ColorLib.gray(200)));// 根據設定距離hops新建一個圖形距離過濾器類;針對nodes,采取完全填充顏色的方式(FILLCOLOR),並對聚焦點(fixed )、高亮點(與fixed node相鄰的點即highlight)以及剩余點分別賦予不同的顏色表現.將GraphDistanceFilter和ColorAction都注冊到聲明的ActionList對象上,並同時添加點與邊的描邊顏色以及填充顏色的ColorAction。      
113 
114         ActionList animate = new ActionList(Activity.INFINITY);
115 
116         animate.add(new ForceDirectedLayout(graph));
117 
118         animate.add(fill);
119 
120         animate.add(new RepaintAction());//聲明一個ActionList的animate對象,在該對象上添加布局方式(這里采用力導向布局方法ForceDirectedLayout),並添加上面的ColorAction類的fill對象以及一個重繪圖形Action。
121         // finally, we register our ActionList with the Visualization.
122 
123         // we can later execute our Actions by invoking a method on our
124 
125         // Visualization, using the name we've chosen below.
126 
127         m_vis.putAction("draw", draw);
128 
129         m_vis.putAction("layout", animate);
130 
131         m_vis.runAfter("draw", "layout");//將draw和animate注冊到m_vis上,后面通過Visualization的方法觸發執行每個注冊的Action。
132 
133         // --------------------------------------------------------------------
134 
135         // set up a display to show the visualization
136         Display display = new Display(m_vis);
137         display.setSize(700,700);
138         display.pan(350, 350);
139         display.setForeground(Color.GRAY);
140         display.setBackground(Color.WHITE);
141 
142        
143 
144         // main display controls
145         display.addControlListener(new FocusControl(1));
146         display.addControlListener(new DragControl());
147         display.addControlListener(new PanControl());
148         display.addControlListener(new ZoomControl());
149         display.addControlListener(new WheelZoomControl());
150         display.addControlListener(new ZoomToFitControl());
151         display.addControlListener(new NeighborHighlightControl());//通過Display展現Visualization包括:設置畫布大小,平移范圍,前景背景顏色以及添加聚焦、拖拽、平移、縮放、滑輪、縮放至適合顯示、緊鄰高亮監聽器。
152 
153  
154 
155 // overview display
156 
157 //        Display overview = new Display(vis);
158 
159 //        overview.setSize(290,290);
160 
161 //        overview.addItemBoundsListener(new FitOverviewListener());
162 
163        
164 
165         display.setForeground(Color.GRAY);
166 
167         display.setBackground(Color.WHITE);
168 
169         
170 
171         // --------------------------------------------------------------------       
172 
173         // launch the visualization
174 
175        
176 
177         // create a panel for editing force values
178 
179         ForceSimulator fsim = ((ForceDirectedLayout)animate.get(0)).getForceSimulator();
180 
181         JForcePanel fpanel = new JForcePanel(fsim);
182 
183        
184 
185         final JValueSlider slider = new JValueSlider("Distance", 0, hops, hops);
186 
187         slider.addChangeListener(new ChangeListener() {
188 
189             public void stateChanged(ChangeEvent e) {
190 
191                 filter.setDistance(slider.getValue().intValue());//只要調節面板上的值有變動就執行下面的run函數,重新布局界面
192 
193                 m_vis.run("draw");
194 
195             }
196 
197         });
198 
199         slider.setBackground(Color.WHITE);
200 
201         slider.setPreferredSize(new Dimension(300,30));
202 
203         slider.setMaximumSize(new Dimension(300,30));//設置調節面板的背景顏色、大小
204 
205  
206 
207        
208 
209         Box cf = new Box(BoxLayout.Y_AXIS);
210 
211         cf.add(slider);
212 
213         cf.setBorder(BorderFactory.createTitledBorder("Connectivity Filter"));
214 
215         fpanel.add(cf);
216 
217         //fpanel.add(opanel);
218 
219         fpanel.add(Box.createVerticalGlue());
220 
221        
222 
223         // create a new JSplitPane to present the interface
224 
225         JSplitPane split = new JSplitPane();
226 
227         split.setLeftComponent(display);
228 
229         split.setRightComponent(fpanel);
230 
231         split.setOneTouchExpandable(true);
232 
233         split.setContinuousLayout(false);
234 
235         split.setDividerLocation(700);//為整張畫布布局,包括左邊、右邊應該呈現什么內容等
236 
237         // now we run our action list
238 
239         //m_vis.run("draw");
240 
241         add(split);
242 
243     }
244 
245    
246 
247     public void setGraph(Graph g, String label) {
248 
249         // update labeling
250 
251         DefaultRendererFactory drf = (DefaultRendererFactory)
252 
253                                                 m_vis.getRendererFactory();
254 
255         ((LabelRenderer)drf.getDefaultRenderer()).setTextField(label);
256 
257         // update graph
258 
259         m_vis.removeGroup(graph);
260 
261         VisualGraph vg = m_vis.addGraph(graph, g);
262 
263         m_vis.setValue(edges, null, VisualItem.INTERACTIVE, Boolean.FALSE);
264 
265         VisualItem f = (VisualItem)vg.getNode(0);
266 
267         m_vis.getGroup(Visualization.FOCUS_ITEMS).setTuple(f);
268 
269         f.setFixed(false);
270 
271     }
272 
273    
274 
275     // ------------------------------------------------------------------------
276 
277     // Main and demo methods
278 
279    
280 
281     public static void main(String[] args) {
282 
283         UILib.setPlatformLookAndFeel();
284 
285        
286 
287         // create graphview
288 
289         String datafile = null;
290 
291         String label = "label";
292 
293         if ( args.length > 1 ) {//如果用戶在運行時有參數傳值則分別賦值給datafile和label
294 
295             datafile = args[0];
296 
297             label = args[1];
298 
299         }
300 
301         JFrame frame = demo(datafile, label); //通過調用demo函數完成整個界面的設計布局等,最終呈現一個JFrame
302 
303         frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 關閉按鈕的動作為退出
304     }
305 
306    
307 
308     public static JFrame demo() {
309 
310         return demo((String)null, "label");
311 
312     }
313 
314    
315 
316     public static JFrame demo(String datafile, String label) {
317 
318         Graph g = null;
319 
320         if ( datafile == null ) {
321 
322             g = GraphLib.getGrid(15,15);//如果datafile為空,則通過調用圖形庫GraphLib中的getGrid得到15*15的網狀圖形,如下圖所示
323             label = "label";
324 
325         } else {
326 
327             try {
328 
329                 g = new GraphMLReader().readGraph(datafile);//否則通過指定路徑讀取datafile文件並轉換為圖形
330 
331             } catch ( Exception e ) {
332 
333                 e.printStackTrace();
334 
335                 System.exit(1);
336 
337             }
338 
339         }
340 
341         return demo(g, label);
342 
343     }
344 
345    
346 
347     public static JFrame demo(Graph g, String label) {
348 
349         final GraphView view = new GraphView(g, label);
350 
351        
352 
353         // set up menu
354 
355         JMenu dataMenu = new JMenu("Data");//新建菜單欄
356 
357         dataMenu.add(new OpenGraphAction(view));//注冊“打開文件”選項卡
358 
359         dataMenu.add(new GraphMenuAction("Grid","ctrl 1",view) {//添加網狀布局選項卡
360 
361             protected Graph getGraph() {
362 
363                 return GraphLib.getGrid(15,15);
364 
365             }
366 
367         });
368 
369         dataMenu.add(new GraphMenuAction("Clique","ctrl 2",view) {//添加團狀布局選項卡
370 
371             protected Graph getGraph() {
372 
373                 return GraphLib.getClique(10);
374 
375             }
376 
377         });
378 
379         dataMenu.add(new GraphMenuAction("Honeycomb","ctrl 3",view) {//添加蜂窩狀布局選項卡
380 
381             protected Graph getGraph() {
382 
383                 return GraphLib.getHoneycomb(5);
384 
385             }
386 
387         });
388 
389         dataMenu.add(new GraphMenuAction("Balanced Tree","ctrl 4",view) {//添加平衡樹布局選項卡
390 
391             protected Graph getGraph() {
392 
393                 return GraphLib.getBalancedTree(3,5);
394 
395             }
396 
397         });
398 
399         dataMenu.add(new GraphMenuAction("Diamond Tree","ctrl 5",view) {
400 
401             protected Graph getGraph() {
402 
403                 return GraphLib.getDiamondTree(3,3,3); //添加鑽石樹形圖布局選項卡
404 
405             }
406 
407         });
408 
409         JMenuBar menubar = new JMenuBar();
410 
411         menubar.add(dataMenu);//將以上菜單選項注冊到menubar菜單欄上
412 
413        
414 
415         // launch window
416 
417         JFrame frame = new JFrame("p r e f u s e  |  g r a p h v i e w");
418 
419         frame.setJMenuBar(menubar);
420 
421         frame.setContentPane(view);
422 
423         frame.pack();
424 
425         frame.setVisible(true);//添加菜單欄、圖形等
426 
427        
428 
429         frame.addWindowListener(new WindowAdapter() {
430 
431             public void windowActivated(WindowEvent e) {
432 
433                 view.m_vis.run("layout");
434 
435             }
436 
437             public void windowDeactivated(WindowEvent e) {
438 
439                 view.m_vis.cancel("layout");
440 
441             }
442 
443         });
444 
445        
446 
447         return frame;
448 
449     }
450 
451 
452     // ------------------------------------------------------------------------
453 
454     /**
455 
456      * Swing menu action that loads a graph into the graph viewer.
457 
458          * 該類主要負責為每一種布局選項配置相應的快捷鍵
459 
460      */
461 
462     public abstract static class GraphMenuAction extends AbstractAction {
463 
464         private GraphView m_view;
465 
466         public GraphMenuAction(String name, String accel, GraphView view) {
467 
468             m_view = view;
469 
470             this.putValue(AbstractAction.NAME, name);
471 
472             this.putValue(AbstractAction.ACCELERATOR_KEY,
473 
474                           KeyStroke.getKeyStroke(accel));
475 
476         }
477 
478         public void actionPerformed(ActionEvent e) {
479 
480             m_view.setGraph(getGraph(), "label");
481 
482         }
483 
484         protected abstract Graph getGraph();
485 
486     }
487 
488     //該類負責對菜單欄的選項卡的響應
489 
490     public static class OpenGraphAction extends AbstractAction {
491 
492         private GraphView m_view;
493 
494  
495 
496         public OpenGraphAction(GraphView view) {
497 
498             m_view = view;
499 
500             this.putValue(AbstractAction.NAME, "Open File...");
501 
502             this.putValue(AbstractAction.ACCELERATOR_KEY,
503 
504                           KeyStroke.getKeyStroke("ctrl O"));
505 
506         }
507 
508         public void actionPerformed(ActionEvent e) {
509 
510             Graph g = IOLib.getGraphFile(m_view);
511 
512             if ( g == null ) return;
513 
514             String label = getLabel(m_view, g);
515 
516             if ( label != null ) {
517 
518                 m_view.setGraph(g, label);
519 
520             }
521 
522         }
523 
524         public static String getLabel(Component c, Graph g) {
525 
526             // get the column names
527 
528             Table t = g.getNodeTable();
529 
530             int  cc = t.getColumnCount();
531 
532             String[] names = new String[cc];
533 
534             for ( int i=0; i<cc; ++i )
535 
536                 names[i] = t.getColumnName(i);
537 
538            
539 
540             // where to store the result
541 
542             final String[] label = new String[1];
543 
544             // -- build the dialog -----
545 
546             // we need to get the enclosing frame first
547 
548             while ( c != null && !(c instanceof JFrame) ) {
549 
550                 c = c.getParent();
551             }
552 
553             final JDialog dialog = new JDialog(
554 
555                     (JFrame)c, "Choose Label Field", true);
556 
557            
558 
559             // create the ok/cancel buttons
560 
561             final JButton ok = new JButton("OK");
562 
563             ok.setEnabled(false);
564 
565             ok.addActionListener(new ActionListener() {
566 
567                public void actionPerformed(ActionEvent e) {
568 
569                    dialog.setVisible(false);
570 
571                }
572 
573             });
574 
575             JButton cancel = new JButton("Cancel");
576 
577             cancel.addActionListener(new ActionListener() {
578 
579                 public void actionPerformed(ActionEvent e) {
580 
581                     label[0] = null;
582 
583                     dialog.setVisible(false);
584 
585                 }
586 
587             });
588 
589             // build the selection list
590 
591             final JList list = new JList(names);
592        list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
593 
594             list.getSelectionModel().addListSelectionListener(
595 
596             new ListSelectionListener() {
597 
598                 public void valueChanged(ListSelectionEvent e) {
599 
600                     int sel = list.getSelectedIndex();
601 
602                     if ( sel >= 0 ) {
603 
604                         ok.setEnabled(true);
605 
606                         label[0] = (String)list.getModel().getElementAt(sel);
607 
608                     } else {
609 
610                         ok.setEnabled(false);
611 
612                         label[0] = null;
613 
614                     }
615 
616                 }
617 
618             });
619 
620             JScrollPane scrollList = new JScrollPane(list);
621 
622            
623 
624             JLabel title = new JLabel("Choose a field to use for node labels:");
625 
626            
627 
628             // layout the buttons
629 
630             Box bbox = new Box(BoxLayout.X_AXIS);
631 
632             bbox.add(Box.createHorizontalStrut(5));
633 
634             bbox.add(Box.createHorizontalGlue());
635 
636             bbox.add(ok);
637 
638             bbox.add(Box.createHorizontalStrut(5));
639 
640             bbox.add(cancel);
641 
642             bbox.add(Box.createHorizontalStrut(5));
643 
644             
645 
646             // put everything into a panel
647 
648             JPanel panel = new JPanel(new BorderLayout());
649 
650             panel.add(title, BorderLayout.NORTH);
651 
652             panel.add(scrollList, BorderLayout.CENTER);
653 
654             panel.add(bbox, BorderLayout.SOUTH);
655 
656             panel.setBorder(BorderFactory.createEmptyBorder(5,2,2,2));
657 
658            
659 
660             // show the dialog
661 
662             dialog.setContentPane(panel);
663 
664             dialog.pack();
665 
666             dialog.setLocationRelativeTo(c);
667 
668             dialog.setVisible(true);
669 
670             dialog.dispose();
671 
672            
673 
674             // return the label field selection
675 
676             return label[0];
677 
678         }
679 
680     }
681 
682     //該類負責調整至適合屏幕顯示
683 
684     public static class FitOverviewListener implements ItemBoundsListener {
685 
686         private Rectangle2D m_bounds = new Rectangle2D.Double();
687 
688         private Rectangle2D m_temp = new Rectangle2D.Double();
689 
690         private double m_d = 15;
691 
692         public void itemBoundsChanged(Display d) {
693 
694             d.getItemBounds(m_temp);
695 
696             GraphicsLib.expand(m_temp, 25/d.getScale());
697 
698            
699 
700             double dd = m_d/d.getScale();
701 
702             double xd = Math.abs(m_temp.getMinX()-m_bounds.getMinX());
703 
704             double yd = Math.abs(m_temp.getMinY()-m_bounds.getMinY());
705 
706             double wd = Math.abs(m_temp.getWidth()-m_bounds.getWidth());
707 
708             double hd = Math.abs(m_temp.getHeight()-m_bounds.getHeight());
709 
710             if ( xd>dd || yd>dd || wd>dd || hd>dd ) {
711 
712                 m_bounds.setFrame(m_temp);
713 
714                 DisplayLib.fitViewToBounds(d, m_bounds, 0);
715 
716             }
717 
718         }
719 
720     }
721 
722    
723 
724 }
725 
726  // end of class GraphView

網格視圖:

蜂窩狀視圖:

平衡樹型視圖:

 

以上介紹了Prefuse的一些特征,模型結構以及自帶Demo GraphView.java的理解,后續會繼續研究Prefuse的其他Demo以及主要接口。

本文鏈接http://www.cnblogs.com/bigdataZJ/p/VisualizationSoloShow3.html

 

友情贊助

如果你覺得博主的文章對你那么一點小幫助,恰巧你又有想打賞博主的小沖動,那么事不宜遲,趕緊掃一掃,小額地贊助下,攢個奶粉錢,也是讓博主有動力繼續努力,寫出更好的文章^^。

    1. 支付寶                          2. 微信

                      


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM