前言

  我写Delphi程序是从MIS系统入门的,开始尝试子系统划分的时候采用的是MDI窗体的结构。随着系统功能的扩充,不断有新的子系统加入系统中,单个工程会变得非常大,每次做一点修改都要重新编译,单个工程的形式也不利于团队协作。为了提高工作效率,我希望利用DLL动态链接库的形式实现插件结构的编程。

  插件结构的编程需要一个插件容器来控制各DLL的运行情况,将划分好的每个子系统安排到一个DLL库文件中。对每个DLL程序需要为容器预留接口函数,一般接口函数包括:启动调用DLL库的函数、关闭DLL库的函数。通过接口函数,插件容器可以向DLL模块传递参数实现动态控制。具体实现细节我将在下文说明并给出响应代码。

  您可能需要先了解一下DELPHI中UNIT的结构,工程的结构。本文没有深入讨论DLL编程的理论细节,只是演示了一些实用的代码,我当时学习的是刘艺老师的《DELPHI深入编程》一书。

  我也处于DELPHI的入门阶段,只是觉得这次的DLL开发有一些值得讨论的地方,所以写这篇文章,希望各位能对我做的不好的地方慷慨建议。

  示例程序简介

  为了便于阅读我将使用一个MIS系统的部分程序代码演示插件编程的一些方法。示例程序是典型的C/S结构DBMS应用程序,我们关注的部分将是框架程序(下文简称Hall)的控制语句和dll插件程序的响应控制。

  1、程序结构

  插件容器Hall使用一个独立的工程创建,Hall的主窗口的作用相当于MDI程序中的MDI容器窗体,Hall中将显式调用Dll中的接口函数。
每个插件程序独立使用各自的工程,与普通工程不同的是,DLL工程创建的是Dll Wizard,相应编译生成的文件是以DLL为后缀。

  2、接口设计

  实例程序Narcissus中我们预留两个接口函数:

  ShowDLLForm

  该函数将应用程序的句柄传递给DLL子窗口,DLL程序将动态创建DLL窗体的实例。还可以将一些业务逻辑用参数的形式传递给DLL子窗口,比如窗体名称、当前登陆的用户名等。初次调用一个DLL窗体实例时使用此函数创建。

  FreeDLLForm

  该函数将显示释放DLL窗口实例,在退出应用程序时调用每个DLL窗体的FreeDLLForm方法来释放创建的实例,不然会引起内存只读错误。同样,也可以将一些在释放窗体时需要做的业务逻辑用参数的形式传递给DLL窗体。

  3、调试方式

  DLL窗体程序无法直接执行,需要有一个插件容器来调用。应此我们需要先实现一个基本的Hall程序,然后将Hall.exe保存在一个固定的目录中。对每个DLL工程做如下设置:

  1) 打开DLL工程

  2) 选择菜单 Run – Parameters

  3) 在弹出的窗口中浏览到我们的容器Hall.exe

  这样在调试DLL程序时将会自动调用Hall程序,利用Hall中预留的调用接口调试DLL程序。

插件程序的基本实现

  DLL程序的设计方式和普通WINAPP没有很大的区别,只是所有的窗口都是作为一种特殊的“资源”保存在DLL库中,需要手动调用,而不像WINAPP中会有工程自动创建。声明接口函数的方法很简单

  1) 在Unit的Implementation部分中声明函数

  2) 在函数声明语句的尾部加上stdcall标记

  3) 在工程代码(Project – View Source)的begin语句之前,用exports语句声明函数接口

  为了使代码简洁,我个人喜欢在工程中独立添加一个Unit单元(File – New -- Unit),然后将所有要输出的函数体定义在此单元中,不要忘记将引用到的窗体的Unit也uses进来。我命名这个单元为UnitEntrance,在ShowDLLForm函数中初始化了要显示的窗口并调用Show方法显示,HALL会将登陆的用户名用参数传递过来,得到用户名后就可以进行一些权限控制,表现在界面初始化上。

  其代码如下

  1. unit UnitOfficeEntrance;
  2.  
  3. interface
  4.  
  5. uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls,
  6. Forms;
  7.  
  8. function ShowDLLForm( AHandle : THandle; ACaption : string; AUserID : string )
  9. : boolean; stdcall;
  10.  
  11. function FreeDLLForm( AHandle : THandle; ACaption : string; AUserID : string )
  12. : boolean; stdcall;
  13.  
  14. implementation
  15.  
  16. uses UnitOfficialMainForm; // 改成MAINFORM的unit
  17.  
  18. var
  19. DLL_Form : TFormOfficialMain; // 改成MAINFORM的NAME
  20.  
  21. // -----------------------------------------
  22. // Name: ShowDLLForm
  23. // Func: DLL插件调用入口函数
  24. // Para: AHandle 挂靠程序句柄; ACaption 本窗体标题
  25. // Rtrn: N/A
  26. // Auth: CST
  27. // Date: --
  28. // -----------------------------------------
  29.  
  30. function ShowDLLForm( AHandle : THandle; ACaption : string; AUserID : string )
  31. : boolean;
  32.  
  33. begin
  34. result := true;
  35.  
  36. try
  37. Application.Handle := AHandle; // 挂靠到主程序容器
  38. DLL_Form := TFormOfficialMain.Create( Application ); // 改成MAINFORM的NAME
  39.  
  40. try
  41. with DLL_Form do
  42. begin
  43. Caption := ACaption;
  44. StatusBar.Panels.Items[ ].Text := AUserID;
  45. // Configure UI
  46. Show;
  47.  
  48. end;
  49.  
  50. except
  51. on e : exception do
  52. begin
  53. DLL_Form.Free;
  54.  
  55. end;
  56.  
  57. end;
  58.  
  59. except
  60. result := false;
  61.  
  62. end;
  63.  
  64. end;
  65.  
  66. // -----------------------------------------
  67. // Name: FreeDLLForm
  68. // Func: DLL插件调用出口函数
  69. // Para: AHandle 挂靠程序句柄
  70. // Rtrn: true/false
  71. // Auth: CST
  72. // Date: --
  73. // -----------------------------------------
  74.  
  75. function FreeDLLForm( AHandle : THandle; ACaption : string; AUserID : string )
  76. : boolean;
  77. begin
  78. Application.Handle := AHandle; // 挂靠到主程序容器
  79.  
  80. if DLL_Form.Showing then
  81. DLL_Form.Close; // 如果窗口打开先关闭,触发FORM.CLOSEQUERY可取消关闭过程
  82.  
  83. if not DLL_Form.Showing then
  84. begin
  85. DLL_Form.Free;
  86. result := true;
  87.  
  88. end // 仍然打开状态,说明CLOSEQUERY.CANCLOSE=FALSE
  89.  
  90. else
  91. begin
  92. result := false;
  93.  
  94. end;
  95. end;
  96.  
  97. end.

DLL工程文件代码如下:

  1. library Official;
  2.  
  3. { Important note about DLL memory management: ShareMem must be the
  4.  
  5. first unit in your librarys USES clause AND your projects (select
  6.  
  7. Project-View Source) USES clause if your DLL exports any procedures or
  8.  
  9. functions that pass strings as parameters or function results. This
  10.  
  11. applies to all strings passed to and from your DLL--even those that
  12.  
  13. are nested in records and classes. ShareMem is the interface unit to
  14.  
  15. the BORLNDMM.DLL shared memory manager, which must be deployed along
  16.  
  17. with your DLL. To avoid using BORLNDMM.DLL, pass string information
  18.  
  19. using PChar or ShortString parameters. }
  20.  
  21. uses
  22.  
  23. SysUtils,
  24.  
  25. Classes,
  26.  
  27. UnitOfficialDetailForm in UnitOfficialDetailForm.pas ’,
  28.  
  29. UnitOfficialMainForm in UnitOfficialMainForm.pas ’,
  30.  
  31. UnitOfficeEntrance in UnitOfficeEntrance.pas ’,
  32.  
  33. UnitOfficialClass in .. .. PublicLibraryUnitOfficialClass.pas ’,
  34.  
  35. UnitMyDataAdatper in .. .. PublicLibraryUnitMyDataAdatper.pas ’,
  36.  
  37. UnitMyHeaders in .. .. PublicLibraryUnitMyHeaders.pas ’;
  38.  
  39. {$R *.res}
  40.  
  41. exports ShowDLLForm, FreeDLLForm; // 接口函数
  42.  
  43. begin
  44.  
  45. end.

插件程序一旦调用了DLL窗口,窗口实例将会保持在HALL窗口的上层,因此不用担心遮挡的问题。

容器程序的实现

  1、接口函数的引入

  调用DLL库中的函数有显式和隐式两种方式,显式调用更灵活,因此我们使用显示调用。在Delphi中需要为接口函数申明函数类型,然后实例化函数类型的实例,该实例实际是一个指向函数的指针,通过指针我们可以访问到函数并传递参数、获取返回值。在单元文件的Interface部分加入函数类的申明:

  1. type
  2.  
  3. //定义接口函数类型,接口函数来自DLL接口
  4.  
  5. TShowDLLForm = Function(AHandle:THandle; ACaption: String; AUserID:string):Boolean;stdcall;
  6.  
  7. TFreeDLLForm = Function(AHandle:THandle; ACaption: String; AUserID:string):boolean;stdcall;

显示调用库函数需要如下几个步骤:

  1) 载入DLL库文件

  2) 获得函数地址

  3) 执行函数

  4) 释放DLL库

  接下来我们将详细讨论这几个步骤。

  2、载入DLL库文件

  通过调用API函数LoadLibrary可以将DLL库载入到内存中,在此我们不讨论DLL对内存管理的影响。LoadLibrary的参数是DLL文件的地址路径,如果载入成功会返回一个CARDINAL类型的变量作为DLL库的句柄;如果目标文件不存在或其他原因导致载入DLL文件失败会返回一个0。

  3、实例化接口函数

  获得接口函数指针的API函数为GetProcAddress(库文件句柄,函数名称),如果找到函数则会返回该函数的指针,如果失败则返回NIL。
使用上文定义的函数类型定义函数指针变量,然后使用@操作符获得函数地址,这样就可以使用指针变量访问函数。主要代码如下:

  1. var
  2.  ShowDLLForm: TShowDLLForm; //DLL接口函数实例
  3.  FreeDLLForm: TFreeDLLForm;
  4. begin
  5.  try
  6.  begin
  7.   APlugin.ProcAddr := LoadLibrary(PChar(sPath));
  8.   APlugin.FuncFreeAddr := GetProcAddress(APlugin.ProcAddr,’FreeDLLForm’);
  9.   APlugin.FuncAddr := GetProcAddress(APlugin.ProcAddr ,’ShowDLLForm’);
  10.  
  11.   @ShowDLLForm:=APlugin.FuncAddr ;
  12.   @FreeDLLForm:=APlugin.FuncFreeAddr;
  13.   if ShowDllForm(Self.Handle, APlugin.Caption , APlugin.UserID) then
  14.    Result:=True

4、一个具体的实现方法

  为了结构化管理插件,方便今后的系统扩充,我们可以结合数据库记录可用的DLL信息,然后通过查询数据库记录动态访问DLL程序。

  1) 系统模块表设计

  对于MIS系统,可以利用已有的DBS条件建立一个系统模块表,记录DLL文件及映射到系统模块中的相关信息

字段名 作用 类型
AutoID 索引 INT
modAlias 模块别称 VARCHAR
modName 模块名称 VARCHAR
modWndClass 窗体唯一标识 VARCHAR
modFile DLL路径 VARCHAR
modMemo 备注 TEXT

  ·模块别称是用来在编程设计阶段统一命名的规则,特别是团队开发时可以供队员参考。

  ·模块名称将作为ACAPTION参数传递给SHOWDLLFORM函数作为DLL窗口的标题。

  ·窗体唯一标识是DLL子模块中主窗口的CLASSNAME,用来在运行时确定要控制的窗口。

  ·DLL路径保存DLL文件名称,程序中将转换为绝对路径。

  2) 插件信息数据结构

  定义一个记录插件相关信息的数据接口可以集中控制DLL插件。在Interface部分加入如下代码:

  1. type
  2.  
  3.  //定义插件信息类
  4.  
  5.  TMyPlugins = class
  6.  Caption:String; //DLL窗体标题
  7.  DllFileName:String; //DLL文件路径
  8.  WndClass:String; //窗体标识
  9.  UserID:string; //用户名
  10.  ProcAddr:THandle; //LOADLIBRARY载入的库句柄
  11.  FuncAddr:Pointer; //SHOWDLLFORM函数指针
  12.  FuncFreeAddr:Pointer; //FREEDLLFORM函数指针
  13. end;

为每个插件创建一个TMyPlugins的实例,下文会讨论对这些实例的初始化方法。

  3) 插件载入函数

  在本示例中DLL窗口是在HALL中触发打开子窗口的事件中载入并显示的。按钮事件触发后,先根据插件结构体实例判断DLL是否已经加载,如果已经加载,则控制窗口的显示或关闭;如果没有加载则访问数据表将字段赋值到插件结构体中,然后执行载入、获得指针的工作。

  局部代码如下

  1. //-----------------------------------------
  2.  
  3. //Name: OpenPlugin
  4.  
  5. //Func: 插件信息类控制过程: 初始化==》设置权限==》载入DLL窗口
  6.  
  7. //Para: APlugin-TMyPlugins; sAlias别名; iFuncValue权限值
  8.  
  9. //Rtrn: N/A
  10.  
  11. //Auth: CST
  12.  
  13. //Date: --
  14.  
  15. //-----------------------------------------
  16.  
  17. procedure TFormHall.OpenPlugin(AFromActn: TAction ;APlugin:TMyPlugins; sAlias:string; sUserID:string);
  18.  var hWndPlugin:HWnd;
  19. begin
  20.  
  21.  //判断插件窗口是否已经载入 hWndPlugin:=FindWindow(PChar(APlugin.WndClass),nil);
  22.  if hWndPlugin <> then //插件窗口已经载入
  23.  begin
  24.   if not IsWindowVisible(hWndPlugin) then
  25.   begin
  26.    AFromActn.Checked := True;
  27.    ShowWindow(hWndPlugin,SW_SHOWDEFAULT); //显示
  28.   end
  29.   else
  30.   begin
  31.    AFromActn.checked := False;
  32.    ShowWindow(hWndPlugin,SW_HIDE) ;
  33.   end;
  34.   Exit; //离开创建插件过程
  35.  end;
  36.  
  37. //初始化插件类实例
  38.  
  39. if not InitializeMyPlugins(APlugin,sAlias) then
  40. begin
  41.  showmessage(’初始化插件类错误。’);
  42.  exit;
  43. end;
  44.  
  45. //获得当前权限值
  46.  
  47. APlugin.UserID := sUserID;
  48. //载入DLL窗口
  49.  
  50. if not LoadShowPluginForm(APlugin) then
  51. begin
  52.  showmessage(’载入中心插件出错。’);
  53.  exit;
  54.  end;
  55. end;
  56.  
  57. //-----------------------------------------
  58. //Name: InitializeMyPlugins
  59. //Func: 初始化MYPLUGIN实例 (Caption | DllFileName | IsLoaded)
  60. //Para: APlugin-TMyPlugins
  61. //Rtrn: N/A
  62. //Auth: CST
  63. //Date: --
  64. //-----------------------------------------
  65.  
  66. function TFormHall.InitializeMyPlugins(APlugin:TMyPlugins; sAlias:String):Boolean;
  67. var
  68.  strSQL:string;
  69.  myDA:TMyDataAdapter;
  70. begin
  71.  Result:=False;
  72.  myDA:=TMyDataAdapter.Create;
  73.  strSQL:=’SELECT * FROM SystemModuleList WHERE modAlias=’+QuotedStr(sAlias);
  74.  try
  75.   myDA.RetrieveData(strSQL);
  76.  except
  77.   on E:Exception do
  78.   begin
  79.    result:=false;
  80.    myDA.Free ;
  81.    exit;
  82.   end;
  83.  end;
  84. try
  85.  begin
  86.   with myDA.MyDataSet do
  87.  begin
  88.   if Not IsEmpty then
  89.  begin
  90.   APlugin.Caption:= FieldByName(’modName’).Value;
  91.   APlugin.DllFileName := FieldByName(’modFile’).Value;
  92.   APlugin.WndClass := FieldByName(’modWndClass’).Value ;
  93.   result:=True;
  94.  end;
  95. Close;
  96.  end; //end of with...do...
  97.  end; //end of try
  98.  except
  99.   on E:Exception do
  100. begin
  101.  Result:=False;
  102.  myDA.Free ;
  103.  Exit;
  104.  end; //end of exception
  105. end; //end of try...except
  106.  
  107.  myDA.Free ;
  108. end;
  109.  
  110. //-----------------------------------------
  111.  
  112. //Name: LoadShowPluginForm
  113.  
  114. //Func: 载入DLL插件并显示窗口
  115.  
  116. //Para: APlugin-TMyPlugins
  117.  
  118. //Rtrn: true-创建成功
  119.  
  120. //Auth: CST
  121.  
  122. //Date: --
  123.  
  124. //-----------------------------------------
  125.  
  126. function TFormHall.LoadShowPluginForm (const APlugin:TMyPlugins):boolean;
  127.  
  128. var
  129.  ShowDLLForm: TShowDLLForm; //DLL接口函数实例
  130.  FreeDLLForm: TFreeDLLForm;
  131.  sPath:string; //DLL文件的完整路径
  132. begin
  133.  try
  134.  begin
  135.   sPath:=ExtractFilepath(Application.ExeName)+ plugins + APlugin.DllFileName ;
  136.   APlugin.ProcAddr := LoadLibrary(PChar(sPath));
  137.   APlugin.FuncFreeAddr := GetProcAddress(APlugin.ProcAddr,’FreeDLLForm’);
  138.   APlugin.FuncAddr := GetProcAddress(APlugin.ProcAddr ,’ShowDLLForm’);
  139.   @ShowDLLForm:=APlugin.FuncAddr ;
  140.   @FreeDLLForm:=APlugin.FuncFreeAddr;
  141.   if ShowDllForm(Self.Handle, APlugin.Caption , APlugin.UserID) then
  142.    Result:=True
  143.   else
  144.    Result:=False;
  145.   end;
  146.   except
  147.    on E:Exception do
  148.   begin
  149.    Result:=False;
  150.    ShowMessage(’载入插件模块错误,请检查PLUGINS目录里的文件是否完整。’);
  151.   end;
  152.  end;
  153. end;

4) DLL窗口控制

  正如3)中的代码说明的那样,DLL窗口的打开和关闭只是在表象层,关闭窗口并没有真正释放DLL窗口,只是调用API函数FindWindow根据窗口标识(就是Form.name)获得窗体句柄,用SHOWWINDOW函数的nCmdShow参数控制窗口显示/隐藏。

  其实这是我这个程序实现的不好的一个地方,如果在DLL窗口中使用Self.close方法会引起内存错误,实在能力有限没有办法解决,因此出此下策。所以每个DLL程序主窗口的关闭按钮都必须隐藏掉。 :-P

  5) DLL库的释放

  在程序退出时,必须根据插件信息实例逐一释放DLL库。释放DLL库的函数如下:

  1. procedure TFormHall.ClosePlugin(aPLG:TMyPlugins);
  2. var
  3.  FreeDLLForm:TFreeDLLForm;
  4. begin
  5.  if aPLG.ProcAddr = then exit;
  6.  if aPLG.FuncFreeAddr = nil then exit;
  7.  @FreeDLLForm:=aPLG.FuncFreeAddr;
  8.  if not FreeDLLForm(Application.Handle,’’,’’) then
  9.   showMessage(’err’);
  10. end;

  小结

   我以上的方法中,因为有不少能力有限没有解决的问题,所以采用了一些看起来不太合理的掩饰方法,希望大家能在做了一点尝试后设计出更好的解决方法,我也希望能学到更多的好方法。

初探Delphi中的插件编程的更多相关文章

  1. 转发 Delphi中线程类TThread 实现多线程编程

    Delphi中有一个线程类TThread是用来实现多线程编程的,这个绝大多数Delphi书藉都有说到,但基本上都是对TThread类的几个成员作一简单介绍,再说明一下Execute的实现和Synchr ...

  2. Delphi中任务栏状态区的编程

    在Windows桌面的任务栏上有一个凹陷的区域,其中显示着系统时钟以及一些图标,这个长方形的区域便是Windows的任务栏状态区(taskbar status area).本文将介绍使用Borland ...

  3. Delphi中线程类TThread实现多线程编程2---事件、临界区、Synchronize、WaitFor……

    接着上文介绍TThread. 现在开始说明 Synchronize和WaitFor 但是在介绍这两个函数之前,需要先介绍另外两个线程同步技术:事件和临界区 事件(Event) 事件(Event)与De ...

  4. delphi中formatFloat代码初探(在qt下实现floatformat的函数)

    由于项目需要,需要在qt下实现floatformat的函数.之前写过一个,但是写得不好.决定重新写一个,参考delphi xe2下的实现.把xe2下的相关代码都看了一遍,xe2的代码思路在这里贴出来. ...

  5. Delphi中正常窗口的实现

    摘要: 在Delphi的VCL库中,为了使用以及实现的方便,应用对象Application创建了一个用来处理消息响应的隐藏窗口.而正是这个窗口,使得用VCL开发出来的程序存在着与其他窗口不能正常排列平 ...

  6. 分享在winform下实现模块化插件编程-优化版

    上一篇<分享在winform下实现模块化插件编程>已经实现了模块化编程,但我认为不够完美,存在以下几个问题: 1.IAppContext中的CreatePlugInForm方法只能依据完整 ...

  7. 分享在winform下实现模块化插件编程

    其实很早之前我就已经了解了在winform下实现插件编程,原理很简单,主要实现思路就是:先定一个插件接口作为插件样式及功能的约定,然后具体的插件就去实现这个插件接口,最后宿主(应用程序本身)就利用反射 ...

  8. Delphi中使用比较少的一些语法

    本文是为了加强记忆而写,这里写的大多数内容都是在编程的日常工作中使用频率不高的东西,但是又十分重要. ---Murphy 1,构造和析构函数: a,构造函数: 一般基于TComponent组件的派生类 ...

  9. jQuery 插件编程精讲与技巧

    适应的读者: 1.有一定的jquery编程基础但是想在技能上有所提升的人 2.前端开发的程序员 3.对编程感兴趣的学生 为什么要学习jquery插件的编写? 为什么要学习jquery插件的编写?相信这 ...

随机推荐

  1. HDU 6205 2017沈阳网络赛 思维题

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=6205 题意:给你n堆牌,原本每一堆的所有牌(a[i]张)默认向下,每次从第一堆开始,将固定个数的牌(b ...

  2. Linux软件安装install命令

    install  1.作用 install命令的作用是安装或升级软件或备份数据,它的使用权限是所有用户. 2.格式 (1)install [选项]... 来源 目的地 (2)install [选项]. ...

  3. python_线程、进程和协程

    线程 Threading用于提供线程相关的操作,线程是应用程序中工作的最小单元. #!/usr/bin/env python #coding=utf-8 __author__ = 'yinjia' i ...

  4. CEPH 使用SSD日志盘+SATA数据盘, 随OSD数目递增对性能影响的递增测试

    最近建设新机房,趁项目时间空余较多,正好系统的测试一下CEPH集群性能随OSD数目的变化情况, 新ceph集群测试结果如下: 1)4k随机读在3/6/9osd host下的性能差不多,吞吐量约50~6 ...

  5. android4.0 锁屏实现(转)

    转载请表明出处:http://blog.csdn.net/wdaming1986/article/details/8837023 好了,言归正传,说说锁屏了,其实把锁屏做成apk的形式,会引起很多问题 ...

  6. 用strtok函数分割字符串

    用strtok函数分割字符串 需要在loadrunner里面获得“15”(下面红色高亮的部分),并做成关联参数. //Body response 内容: <BODY><; PRE&g ...

  7. lr_start_transaction/lr_end_transaction事物组合

    lr_start_transaction/lr_end_transaction事物组合 总结一下: lr_start_transaction与lr_end_transaction 为使用最多的事物创造 ...

  8. volatile 的一个经典例子

    volatile 关键字的两层语义 一旦一个共享变量(类的成员变量.类的静态成员变量)被 volatile 修饰之后,那么就具备了两层语义: 1)保证了不同线程对这个变量进行操作时的可见性,即一个线程 ...

  9. PHP包管理

    前言 在nodejs中,存在npm,python中也存在pip,而php之前不存在类似的东西,导致想要安装一个包,只能去复制代码,但是现在,使用composer可以简单的安装一个包(但是compose ...

  10. Mac 命令行美化

    在 mac 中使用原生的命令行工具,竟然没有 git 命令的自动补全,在 git 仓库下也看不到当前的分支名,不能忍.于是,开始一波改造. 目标:命名 Tab 自动补全:可以显示分支名: 一番 Goo ...