可视化工具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加的一些注释:
//start of class GraphView
public class GraphView extends JPanel {
private static final String graph = "graph";
private static final String nodes = "graph.nodes";
private static final String edges = "graph.edges";
private Visualization m_vis;
public GraphView(Graph g, String label) {
super(new BorderLayout());//在GraphView的构造函数中调用超类的构造方法,并创建布局BorderLayout对象。
// create a new, empty visualization for our data
m_vis = new Visualization();//创建Visualization对象,使用默认的渲染工厂(DefaultRendererFactory)。Visualization类负责管理源数据与可视化组件之间的映射。
// --------------------------------------------------------------------
// set up the renderers
LabelRenderer tr = new LabelRenderer();
tr.setRoundedCorner(8, 8);
m_vis.setRendererFactory(new efaultRendererFactory(tr));//新建标签渲染器并注册到Visualization上,使用的还是DefaultRendererFactory。
// --------------------------------------------------------------------
// register the data with a visualization
// adds graph to visualization and sets renderer label field
setGraph(g, label);// 向Visualization添加图形Graph并为标签域赋值。
// fix selected focus nodes 声明一个数据元组集合,并为该集合添加一个数据元组的监听器
TupleSet focusGroup = m_vis.getGroup(Visualization.FOCUS_ITEMS);
focusGroup.addTupleSetListener(new TupleSetListener() {
public void tupleSetChanged(TupleSet ts, Tuple[] add, Tuple[] rem)
{
for ( int i=0; i<rem.length; ++i )
((VisualItem)rem[i]).setFixed(false);
for ( int i=0; i<add.length; ++i ) {
((VisualItem)add[i]).setFixed(false);
((VisualItem)add[i]).setFixed(true);
}
if ( ts.getTupleCount() == 0 ) {
ts.addTuple(rem[0]);
((VisualItem)rem[0]).setFixed(false);
}
m_vis.run("draw");
}
});//声明一个数据元组集合,并通过匿名内部类的形式为该集合添加一个数据元组的监听器(TupleSetListener),其中ts:变化的数据元组;add:已经加入的元组数组集合;rem:移除的数据集合。
// --------------------------------------------------------------------
// create actions to process the visual data
int hops = 30;
final GraphDistanceFilter filter = new GraphDistanceFilter(graph, hops);
ColorAction fill = new ColorAction(nodes,
VisualItem.FILLCOLOR, ColorLib.rgb(200,200,255));
fill.add(VisualItem.FIXED, ColorLib.rgb(255,100,100));
fill.add(VisualItem.HIGHLIGHT, ColorLib.rgb(255,200,125));
ActionList draw = new ActionList();
draw.add(filter);
draw.add(fill);
draw.add(new ColorAction(nodes, VisualItem.STROKECOLOR, 0));
draw.add(new ColorAction(nodes, VisualItem.TEXTCOLOR, ColorLib.rgb(0,0,0)));
draw.add(new ColorAction(edges, VisualItem.FILLCOLOR, ColorLib.gray(200)));
draw.add(new ColorAction(edges, VisualItem.STROKECOLOR, ColorLib.gray(200)));// 根据设定距离hops新建一个图形距离过滤器类;针对nodes,采取完全填充颜色的方式(FILLCOLOR),并对聚焦点(fixed )、高亮点(与fixed node相邻的点即highlight)以及剩余点分别赋予不同的颜色表现.将GraphDistanceFilter和ColorAction都注册到声明的ActionList对象上,并同时添加点与边的描边颜色以及填充颜色的ColorAction。
ActionList animate = new ActionList(Activity.INFINITY);
animate.add(new ForceDirectedLayout(graph));
animate.add(fill);
animate.add(new RepaintAction());//声明一个ActionList的animate对象,在该对象上添加布局方式(这里采用力导向布局方法ForceDirectedLayout),并添加上面的ColorAction类的fill对象以及一个重绘图形Action。
// finally, we register our ActionList with the Visualization.
// we can later execute our Actions by invoking a method on our
// Visualization, using the name we've chosen below.
m_vis.putAction("draw", draw);
m_vis.putAction("layout", animate);
m_vis.runAfter("draw", "layout");//将draw和animate注册到m_vis上,后面通过Visualization的方法触发执行每个注册的Action。
// --------------------------------------------------------------------
// set up a display to show the visualization
Display display = new Display(m_vis);
display.setSize(700,700);
display.pan(350, 350);
display.setForeground(Color.GRAY);
display.setBackground(Color.WHITE);
// main display controls
display.addControlListener(new FocusControl(1));
display.addControlListener(new DragControl());
display.addControlListener(new PanControl());
display.addControlListener(new ZoomControl());
display.addControlListener(new WheelZoomControl());
display.addControlListener(new ZoomToFitControl());
display.addControlListener(new NeighborHighlightControl());//通过Display展现Visualization包括:设置画布大小,平移范围,前景背景颜色以及添加聚焦、拖拽、平移、缩放、滑轮、缩放至适合显示、紧邻高亮监听器。
// overview display
// Display overview = new Display(vis);
// overview.setSize(290,290);
// overview.addItemBoundsListener(new FitOverviewListener());
display.setForeground(Color.GRAY);
display.setBackground(Color.WHITE);
// --------------------------------------------------------------------
// launch the visualization
// create a panel for editing force values
ForceSimulator fsim = ((ForceDirectedLayout)animate.get(0)).getForceSimulator();
JForcePanel fpanel = new JForcePanel(fsim);
final JValueSlider slider = new JValueSlider("Distance", 0, hops, hops);
slider.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
filter.setDistance(slider.getValue().intValue());//只要调节面板上的值有变动就执行下面的run函数,重新布局界面
m_vis.run("draw");
}
});
slider.setBackground(Color.WHITE);
slider.setPreferredSize(new Dimension(300,30));
slider.setMaximumSize(new Dimension(300,30));//设置调节面板的背景颜色、大小
Box cf = new Box(BoxLayout.Y_AXIS);
cf.add(slider);
cf.setBorder(BorderFactory.createTitledBorder("Connectivity Filter"));
fpanel.add(cf);
//fpanel.add(opanel);
fpanel.add(Box.createVerticalGlue());
// create a new JSplitPane to present the interface
JSplitPane split = new JSplitPane();
split.setLeftComponent(display);
split.setRightComponent(fpanel);
split.setOneTouchExpandable(true);
split.setContinuousLayout(false);
split.setDividerLocation(700);//为整张画布布局,包括左边、右边应该呈现什么内容等
// now we run our action list
//m_vis.run("draw");
add(split);
}
public void setGraph(Graph g, String label) {
// update labeling
DefaultRendererFactory drf = (DefaultRendererFactory)
m_vis.getRendererFactory();
((LabelRenderer)drf.getDefaultRenderer()).setTextField(label);
// update graph
m_vis.removeGroup(graph);
VisualGraph vg = m_vis.addGraph(graph, g);
m_vis.setValue(edges, null, VisualItem.INTERACTIVE, Boolean.FALSE);
VisualItem f = (VisualItem)vg.getNode(0);
m_vis.getGroup(Visualization.FOCUS_ITEMS).setTuple(f);
f.setFixed(false);
}
// ------------------------------------------------------------------------
// Main and demo methods
public static void main(String[] args) {
UILib.setPlatformLookAndFeel();
// create graphview
String datafile = null;
String label = "label";
if ( args.length > 1 ) {//如果用户在运行时有参数传值则分别赋值给datafile和label
datafile = args[0];
label = args[1];
}
JFrame frame = demo(datafile, label); //通过调用demo函数完成整个界面的设计布局等,最终呈现一个JFrame
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 关闭按钮的动作为退出
}
public static JFrame demo() {
return demo((String)null, "label");
}
public static JFrame demo(String datafile, String label) {
Graph g = null;
if ( datafile == null ) {
g = GraphLib.getGrid(15,15);//如果datafile为空,则通过调用图形库GraphLib中的getGrid得到15*15的网状图形,如下图所示
label = "label";
} else {
try {
g = new GraphMLReader().readGraph(datafile);//否则通过指定路径读取datafile文件并转换为图形
} catch ( Exception e ) {
e.printStackTrace();
System.exit(1);
}
}
return demo(g, label);
}
public static JFrame demo(Graph g, String label) {
final GraphView view = new GraphView(g, label);
// set up menu
JMenu dataMenu = new JMenu("Data");//新建菜单栏
dataMenu.add(new OpenGraphAction(view));//注册“打开文件”选项卡
dataMenu.add(new GraphMenuAction("Grid","ctrl 1",view) {//添加网状布局选项卡
protected Graph getGraph() {
return GraphLib.getGrid(15,15);
}
});
dataMenu.add(new GraphMenuAction("Clique","ctrl 2",view) {//添加团状布局选项卡
protected Graph getGraph() {
return GraphLib.getClique(10);
}
});
dataMenu.add(new GraphMenuAction("Honeycomb","ctrl 3",view) {//添加蜂窝状布局选项卡
protected Graph getGraph() {
return GraphLib.getHoneycomb(5);
}
});
dataMenu.add(new GraphMenuAction("Balanced Tree","ctrl 4",view) {//添加平衡树布局选项卡
protected Graph getGraph() {
return GraphLib.getBalancedTree(3,5);
}
});
dataMenu.add(new GraphMenuAction("Diamond Tree","ctrl 5",view) {
protected Graph getGraph() {
return GraphLib.getDiamondTree(3,3,3); //添加钻石树形图布局选项卡
}
});
JMenuBar menubar = new JMenuBar();
menubar.add(dataMenu);//将以上菜单选项注册到menubar菜单栏上
// launch window
JFrame frame = new JFrame("p r e f u s e | g r a p h v i e w");
frame.setJMenuBar(menubar);
frame.setContentPane(view);
frame.pack();
frame.setVisible(true);//添加菜单栏、图形等
frame.addWindowListener(new WindowAdapter() {
public void windowActivated(WindowEvent e) {
view.m_vis.run("layout");
}
public void windowDeactivated(WindowEvent e) {
view.m_vis.cancel("layout");
}
});
return frame;
}
// ------------------------------------------------------------------------
/**
* Swing menu action that loads a graph into the graph viewer.
* 该类主要负责为每一种布局选项配置相应的快捷键
*/
public abstract static class GraphMenuAction extends AbstractAction {
private GraphView m_view;
public GraphMenuAction(String name, String accel, GraphView view) {
m_view = view;
this.putValue(AbstractAction.NAME, name);
this.putValue(AbstractAction.ACCELERATOR_KEY,
KeyStroke.getKeyStroke(accel));
}
public void actionPerformed(ActionEvent e) {
m_view.setGraph(getGraph(), "label");
}
protected abstract Graph getGraph();
}
//该类负责对菜单栏的选项卡的响应
public static class OpenGraphAction extends AbstractAction {
private GraphView m_view;
public OpenGraphAction(GraphView view) {
m_view = view;
this.putValue(AbstractAction.NAME, "Open File...");
this.putValue(AbstractAction.ACCELERATOR_KEY,
KeyStroke.getKeyStroke("ctrl O"));
}
public void actionPerformed(ActionEvent e) {
Graph g = IOLib.getGraphFile(m_view);
if ( g == null ) return;
String label = getLabel(m_view, g);
if ( label != null ) {
m_view.setGraph(g, label);
}
}
public static String getLabel(Component c, Graph g) {
// get the column names
Table t = g.getNodeTable();
int cc = t.getColumnCount();
String[] names = new String[cc];
for ( int i=0; i<cc; ++i )
names[i] = t.getColumnName(i);
// where to store the result
final String[] label = new String[1];
// -- build the dialog -----
// we need to get the enclosing frame first
while ( c != null && !(c instanceof JFrame) ) {
c = c.getParent();
}
final JDialog dialog = new JDialog(
(JFrame)c, "Choose Label Field", true);
// create the ok/cancel buttons
final JButton ok = new JButton("OK");
ok.setEnabled(false);
ok.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
dialog.setVisible(false);
}
});
JButton cancel = new JButton("Cancel");
cancel.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
label[0] = null;
dialog.setVisible(false);
}
});
// build the selection list
final JList list = new JList(names);
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
list.getSelectionModel().addListSelectionListener(
new ListSelectionListener() {
public void valueChanged(ListSelectionEvent e) {
int sel = list.getSelectedIndex();
if ( sel >= 0 ) {
ok.setEnabled(true);
label[0] = (String)list.getModel().getElementAt(sel);
} else {
ok.setEnabled(false);
label[0] = null;
}
}
});
JScrollPane scrollList = new JScrollPane(list);
JLabel title = new JLabel("Choose a field to use for node labels:");
// layout the buttons
Box bbox = new Box(BoxLayout.X_AXIS);
bbox.add(Box.createHorizontalStrut(5));
bbox.add(Box.createHorizontalGlue());
bbox.add(ok);
bbox.add(Box.createHorizontalStrut(5));
bbox.add(cancel);
bbox.add(Box.createHorizontalStrut(5));
// put everything into a panel
JPanel panel = new JPanel(new BorderLayout());
panel.add(title, BorderLayout.NORTH);
panel.add(scrollList, BorderLayout.CENTER);
panel.add(bbox, BorderLayout.SOUTH);
panel.setBorder(BorderFactory.createEmptyBorder(5,2,2,2));
// show the dialog
dialog.setContentPane(panel);
dialog.pack();
dialog.setLocationRelativeTo(c);
dialog.setVisible(true);
dialog.dispose();
// return the label field selection
return label[0];
}
}
//该类负责调整至适合屏幕显示
public static class FitOverviewListener implements ItemBoundsListener {
private Rectangle2D m_bounds = new Rectangle2D.Double();
private Rectangle2D m_temp = new Rectangle2D.Double();
private double m_d = 15;
public void itemBoundsChanged(Display d) {
d.getItemBounds(m_temp);
GraphicsLib.expand(m_temp, 25/d.getScale());
double dd = m_d/d.getScale();
double xd = Math.abs(m_temp.getMinX()-m_bounds.getMinX());
double yd = Math.abs(m_temp.getMinY()-m_bounds.getMinY());
double wd = Math.abs(m_temp.getWidth()-m_bounds.getWidth());
double hd = Math.abs(m_temp.getHeight()-m_bounds.getHeight());
if ( xd>dd || yd>dd || wd>dd || hd>dd ) {
m_bounds.setFrame(m_temp);
DisplayLib.fitViewToBounds(d, m_bounds, 0);
}
}
}
}
// end of class GraphView
网格视图:

蜂窝状视图:

平衡树型视图:

以上介绍了Prefuse的一些特征,模型结构以及自带Demo GraphView.java的理解,后续会继续研究Prefuse的其他Demo以及主要接口。
本文链接http://www.cnblogs.com/bigdataZJ/p/VisualizationSoloShow3.html
友情赞助
如果你觉得博主的文章对你那么一点小帮助,恰巧你又有想打赏博主的小冲动,那么事不宜迟,赶紧扫一扫,小额地赞助下,攒个奶粉钱,也是让博主有动力继续努力,写出更好的文章^^。
1. 支付宝 2. 微信

可视化工具solo show-----Prefuse自带例子GraphView讲解的更多相关文章
- 可视化工具solo show-----Processing Prefuse show
继上篇<可视化工具solo show>罗列出一些主要基于Java开发的软件.插件之后,又仔细揣摩了下哪些可以为我所用. 一番端详之后,准备挑出其中Processing和Prefuse两位大 ...
- 可视化工具solo show
辗转一圈还是回到了我魂牵梦绕的可视化上来了. 在Gephi+Netbeans上折腾了将近一个星期后,我深深的体会到个人对于代码的驾驭能力尚有提升的空间^_^,路很长,方向很重要,三思而行. 转载请标明 ...
- 漫谈可视化Prefuse(五)---一款属于我自己的可视化工具
伴随着前期的基础积累,翻过API,读过一些Demo,总觉得自己已经摸透了Prefuse,小打小闹似乎已经无法满足内心膨胀的自己.还记得儿时看的<武状元苏乞儿>中降龙十八掌最后一张居然是空白 ...
- 可视化工具gephi源码探秘(二)---导入netbeans
在上篇<可视化工具gephi源码探秘(一)>中主要介绍了如何将gephi的源码导入myeclipse中遇到的一些问题,此篇接着上篇而来,主要讲解当下通过myeclipse导入gephi源码 ...
- python 全栈开发,Day63(子查询,MySQl创建用户和授权,可视化工具Navicat的使用,pymysql模块的使用)
昨日内容回顾 外键的变种三种关系: 多对一: 左表的多 对右表一 成立 左边的一 对右表多 不成立 foreign key(从表的id) refreences 主表的(id) 多对多 建立第三张表(f ...
- 可视化工具gephi源码探秘(二)
在上篇<可视化工具gephi源码探秘(一)>中主要介绍了如何将gephi的源码导入myeclipse中遇到的一些问题,此篇接着上篇而来,主要讲解当下通过myeclipse导入gephi源码 ...
- 数据库——可视化工具Navicat、pymysql模块、sql注入问题
数据库--可视化工具Navicat.pymysql模块.sql注入问题 Navicat可视化工具 Navicat是数据库的一个可视化工具,可直接在百度搜索下载安装,它可以通过鼠标"点点点&q ...
- mysql(单表查询,多表查询,MySQl创建用户和授权,可视化工具Navicat的使用)
单表查询 语法: 一.单表查询的语法 SELECT 字段1,字段2... FROM 表名 WHERE 条件 GROUP BY field HAVING 筛选 ORDER BY field LIMIT ...
- MongoDB 安装和可视化工具
MongoDB 是一款非常热门的NoSQL,面向文档的数据库管理系统,官方下载地址是:MongoDB,博主选择的是 Enterprise Server (MongoDB 3.2.9)版本,安装在Win ...
随机推荐
- Linux(centeros)安装weblogic10.3.6教程
http://wenku.baidu.com/link?url=yCLXoDpK7AMdy1_TgGXDncY42Bz6ptdaNq58GdicFWqyI5i-fCvui6mFuIYDt6jkqSiY ...
- zookeeper 的多线程和单线程库使用对比
zookeeper提供了两个库,zookeeper_st和 zookeeper_mt. 前者是单线程库,仅仅提供了异步API和集成在应用程序实现循环中的回调函数,这个库是为了支持pthread库不支持 ...
- Encrypt
begin#BE37F2FB34350DCA6242ADC91F33FA9ABDB6F502F5CA07E3D0CE0C3FAF8799AB3F4622564487B3729F111D9E132A08 ...
- MySQL JDBC 出现多个 SHOW VARIABLES 语句。
一次偶然的机会,show processlist 的时候,发现有个 Client 一直在执行 "mysql-connector-java-5.1.21 ( Revision: ${bzr. ...
- 基于MVC4+EasyUI的Web开发框架形成之旅--附件上传组件uploadify的使用
大概一年前,我还在用Asp.NET开发一些行业管理系统的时候,就曾经使用这个组件作为文件的上传操作,在随笔<Web开发中的文件上传组件uploadify的使用>中可以看到,Asp.NET中 ...
- tsd-提升IDE对JavaScript智能感知的能力
在编写前端JavaScript代码时,最痛苦的莫过于代码的智能感知(Intelli Sense). 追其根源,是因为JavaScript是一门弱类型的动态语言.对于弱类型的动态语言来说,智能感知就是I ...
- 为jQuery的$.ajax设置超时时间
jQuery的ajax模块封装了非常强大的功能,有时候我们在发送一个ajax请求的时候希望能有一个超时的时间,想让程序在一段时间请求不到数据时做出一些反馈.幸运的是jQuery为我们提供了这样的参数: ...
- 推荐windows下的日志跟踪工具:SnakeTail
用过Linux的同学都知道,在Linux中要实时跟踪日志文件那是非常的方便,Tail.Less都可以做到. 开启动态跟踪后,程序会监视文件修改,从而不断刷新出最新的内容,对于线上运维特别有用. 今 ...
- Spring-Context之四:Spring容器及bean的定义
Spring框架的核心功能之一就是控制反转(Inversion of Control, IoC),也叫做依赖注入(dependency injection, DI).关于依赖注入的具体内容可以参见Ma ...
- Git学习笔记(10)——搭建Git服务器
本文主要记录了Git服务器的搭建,以及一些其他的配置,和最后的小总结. Git远程仓库服务器 其实远程仓库和本地仓库没啥不同,远程仓库只是每天24小时开机为大家服务,所以叫做服务器.我们完全可以把自己 ...