`
dananhai
  • 浏览: 91587 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

转 RCP程序-窗口生成

    博客分类:
  • java
阅读更多
eclipse平台下,导入一个应用程序模板后,可以直接运行。这篇文章主要将窗体在生成的过程中有哪些重要的步骤总结了一下。
本篇文章分为那两个部分:
第一个部分为rcp应用程序生成窗体经历的几个步骤。
第二个部分描述窗上尚菜单、工具栏的生成。
1.1        rcp应用程序生成窗体经历的几个步骤:
生成应用程序的窗体,主要经历了以下几个步骤:
1、在application中:创建了工作台
int returnCode = PlatformUI.createAndRunWorkbench(display,
new ApplicationWorkbenchAdvisor());
2、在ApplicationWorkbenchAdvisor中:配置应用程序的窗体
public WorkbenchWindowAdvisor createWorkbenchWindowAdvisor(
           IWorkbenchWindowConfigurer configurer) {
       return new ApplicationWorkbenchWindowAdvisor(configurer);
   }
3、在ApplicationWorkbenchWindowAdvisor类中:配置菜单栏工具栏
public ActionBarAdvisor createActionBarAdvisor(
           IActionBarConfigurer configurer) {
       if (fActionBuilder == null)
       fActionBuilder = new WorkbenchActionBuilder(configurer);
       return fActionBuilder.makeWinAction();
   }
4、在WorkbenchActionBuilder类中:返回不同工作台窗体的ActionBarAdvisor

   public WorkbenchActionBuilder(IActionBarConfigurer Ibarconfigurer) {
       this.Ibarconfigurer = Ibarconfigurer;
   }
      
   public ActionBarAdvisor makeWinAction(){
       switch(WorkbenchControler.flag){
       case WorkbenchControler.main:
           actionBarAdvisor = new MainActionBarAdvisor(Ibarconfigurer);
           break;
       case WorkbenchControler.child_1:
           actionBarAdvisor = new Child_1_ActionBarAdvisor(Ibarconfigurer);
           break;
       case WorkbenchControler.child_2:
           actionBarAdvisor = new MainActionBarAdvisor(Ibarconfigurer);
           break;
       }
       return actionBarAdvisor;
   }
   …
那么eclipse是怎样来帮助我们生成一个主窗体的呢?在这个过程中又经历了哪些过程?现在详细的学习这个过程。
(1)PlatformUI的方法createAndRunWorkbench(),这个方法用来创建工作台。
在(1)中,调用的是PlatformUI的这个方法:
public static int createAndRunWorkbench(Display display,
            WorkbenchAdvisor advisor) {
        return Workbench.createAndRunWorkbench(display, advisor);
    }
通过createAndRunWorkbench来创建工作台窗体,在这个方法中,调用了Workbench了的createAndRunWorkbench这个方法。
(2) Workbench的createAndRunWorkbench()方法
public static final int createAndRunWorkbench(Display display,
           WorkbenchAdvisor advisor) {
       // create the workbench instance
       Workbench workbench = new Workbench(display, advisor);
       // run the workbench event loop
       int returnCode = workbench.runUI();
       return returnCode;
   }
在这个方法中,创建了一个工作台的实例,并把adivsor作为参数传入。然后运行一个Workbench中的一个runUI方法,主要通过这个方法创建了工作台的窗体。我们看看这个方法都实现了什么。
(3)Workbench的runUI()方法
我们看下面这行代码,int returnCode = workbench.runUI();
runUI()的功能就是:用来运行workbench UI(工作台窗体),直到工作台(workbench)关闭和重新启动,它用来承担处理和分派事件。(Internal method for running the workbench UI. This entails processing and dispatching events until the workbench is closed or restarted.)
当正常退出时,返回RETURN_OK,当工作台通过一个针对IWorkbench.restart的访问终止时,返回RETURN_RESTART 当工作台不能被启动时返回RETURN_UNSTARTABLE。其他值留为后用。
在这个runUI()方法中,我们看到一个比较重要的方法:
(4)runUI()方法中的init(display)方法
boolean initOK = init(display);这个方法属于Workbench的内部方法,用来初始化工作台,重建或打开一个窗体。如果init成功,返回initOK。在这个方法中,创建了针对窗体的管理WindowManager,命令的管理CommandManager,上下文的管理ContextManager.
另外在这个方法中还初始化了图像,颜色等。
initializeImages();
initializeFonts();
initializeColors();
initializeApplicationColors();
{通过这些方法,可以学习eclipse是怎么初始化Image,Font,Color等这些比较耗费资源的例子,针对这方面的学习,以后再续。}
在init中,还有个比较重要的方法:通过advisor引用internalBasicInitialize方法:
advisor.internalBasicInitialize(getWorkbenchConfigurer());
我们首先看看它的参数:一个WorkbenchConfiguer对象,getWorkbenchConfigurer()方法在WorkbenchConfiguer类中定义,它返回一个单例WorkbenchConfiguer对象。这个对象用来配置工作台。
public final void internalBasicInitialize(IWorkbenchConfigurer configurer) {
        if (workbenchConfigurer != null) {
            throw new IllegalStateException();
        }
        this.workbenchConfigurer = configurer;
        initialize(configurer);
    }
在internalBasicInitialize将configure对象引用传递给WorkbenchAdvisor对象的workbenchConfigurer属性,并调用了WorkbenchAdvisor类的initialize方法。这个很重要。我们在有关工作台(Workbench)生命周期的文章中提到:initialize方法是在任何窗体打开前调用这个方法。可以用来初始化。我们可以在WorkbenchAdvisor的子类中来实现这个方法。
我们来看看这个方法的官方译文:
在工作台启动之前,执行任意的初始化内容。
在任何窗体被打开之前,工作台初始化时,这个方法会被优先的访问。用户不能直接访问这个方法,,缺省的没有任何的实现。子类可以继承这个方法。用户会用configuer来配置工作台,如果需要用,则用户需要获得通过getWorkbenchConfigurer来获得configurer。
[Performs arbitrary initialization before the workbench starts running.
This method is called during workbench initialization prior to any windows being opened. Clients must not call this method directly (although super calls are okay). The default implementation does nothing. Subclasses may override. Typical clients will use the configurer passed in to tweak the workbench. If further tweaking is required in the future, the configurer may be obtained using getWorkbenchConfigurer. ]
(5)preStartup()方法
在init()这个方法中,还调用了:
advisor.preStartup();
具体的可以参考:前面讲到WorkbenchAdvisor的生命周期中的preStartup方法,它的调用是在第一个窗口打开之前。在启动或者恢复期间暂时禁用某些项时,该方法非常有用。
这个方法在第一个工作台窗体被打开或恢复(重建)时,执行任意的操作。
这个的实在workbench被初始化的之后被访问的,用户不能直接访问这个方法,必须通过子类继承。
[Performs arbitrary actions just before the first workbench window is opened (or restored).
This method is called after the workbench has been initialized and just before the first window is about to be opened. Clients must not call this method directly (although super calls are okay). The default implementation does nothing. Subclasses may override.]
然后再执行下面这个if语句:
if (!advisor.openWindows()) {
               return false;
           }
(6)WorkbenchAdvisor的openWindows()方法
我们看看WorkbenchAdvisor的openWindows()是怎么实现的:
public boolean openWindows() {
        IStatus status = getWorkbenchConfigurer().restoreState();
        if (!status.isOK()) {
            if (status.getCode() == IWorkbenchConfigurer.RESTORE_CODE_EXIT) {
                return false;
            }
            if (status.getCode() == IWorkbenchConfigurer.RESTORE_CODE_RESET) {
                getWorkbenchConfigurer().openFirstTimeWindow();
            }
        }
        return true;
    }
OpenWindows方法的翻译:这个方法用来在在启动时打开工作台窗体。缺省的实现是:通过IWorkbenchConfigurer.restoreWorkbenchState()方法,试图重建(或恢复)先前已保存的工作台状态。如果没有任何先前已保存的状态,或者是重建(恢复)失败,则通过IWorkbenchConfigurer.openFirstTimeWindow来打开第一个窗体。
(Opens the workbench windows on startup. The default implementation tries to restore the previously saved workbench state using IWorkbenchConfigurer.restoreWorkbenchState(). If there was no previously saved state, or if the restore failed, then a first-time window is opened using IWorkbenchConfigurer.openFirstTimeWindow. )
(7)getWorkbenchConfigurer().openFirstTimeWindow()
下面,我们来看看getWorkbenchConfigurer().openFirstTimeWindow()是什么?
getWorkbenchConfigurer()返回一个workbench配置(congifuger)
我们在看看openFirstTimeWindow()方法:它是IWorkbenchConfigurer接口中定义的方法,并由WorkbenchConfigurer实现,实现的方式如下:调用了Workbench的openFirstTimeWindow的方法。
((Workbench) getWorkbench()).openFirstTimeWindow();
我们打开openFirstTimeWindow()方法:
在这个方法中,它又调用了一个doOpenFirstTimeWindow()方法,而这个方法调用了busyOpenWorkbenchWindow方法,这个方法也是Workbench的一个内部方法。它的参数是一个特殊的透视图(Perspective)和一个缺省的workbench page。我们看看他的方法体:
private IWorkbenchWindow busyOpenWorkbenchWindow(String perspID,
           IAdaptable input) throws WorkbenchException {
       // Create a workbench window (becomes active window)
       WorkbenchWindow newWindow = newWorkbenchWindow();
       newWindow.create(); // must be created before adding to window manager
       windowManager.add(newWindow);
       // Create the initial page.
       if (perspID != null) {
           try {
               newWindow.busyOpenPage(perspID, input);
           } catch (WorkbenchException e) {
               windowManager.remove(newWindow);
               throw e;
           }
       }
       // Open window after opening page, to avoid flicker.
       newWindow.open();
       return newWindow;
   }
在这个方法中,用一个newWorkbenchWindow()创建了一个工作台窗体。new了一个WorkbenchWindow对象。
(8)WorkbenchWindow的构造方法。
public WorkbenchWindow(int number) {
        super(null);
        this.number = number;
        // Make sure there is a workbench. This call will throw
        // an exception if workbench not created yet.
        PlatformUI.getWorkbench();
        // Add contribution managers that are exposed to other plugins.
        addMenuBar();
        addCoolBar(SWT.FLAT);
        addStatusLine();
        actionPresentation = new ActionPresentation(this);
        // register with the tracker
        getExtensionTracker()
                .registerHandler(
                        actionSetHandler,
                        ExtensionTracker
                                .createExtensionPointFilter(getActionSetExtensionPoint()));
        fireWindowOpening();
        // set the shell style
        setShellStyle(getWindowConfigurer().getShellStyle());
        // Fill the action bars
        fillActionBars(FILL_ALL_ACTION_BARS);
    }
在这个构造方法中,用addMenuBar()、addCoolBar(SWT.FLAT)、addStatusLine()定义的工作台的菜单,工具栏,还有状态栏。
(9)fireWindowOpening()方法
我们再看看fireWindowOpening();这个方法也是属于WorkbenchWindow。
private void fireWindowOpening() {
        // let the application do further configuration
        getWindowAdvisor().preWindowOpen();
    }
前面讲到,我们每次打开窗体时都需要调用preWindowOpen()这个方法的。
(10)getWindowAdvisor()
我们先看看getWindowAdvisor()方法,这个方法也在WorkbenchWindow类中定义。
private WorkbenchWindowAdvisor getWindowAdvisor() {
        if (windowAdvisor == null) {
            windowAdvisor = getAdvisor().createWorkbenchWindowAdvisor(getWindowConfigurer());
            Assert.isNotNull(windowAdvisor);
        }
        return windowAdvisor;
}
这个方法获得一个单例的WorkbenchWindowAdvisor对象,主要通过WorkbenchAdvistor这个对象的createWorkbenchWindowAdvisor()方法来创建。如上所述,getAdvistor()返回一个WorkbenchAdvisor对象,我们在看看createWorkbenchWindowAdvisor()的参数,它是一个WorkbenchWindowConfigure对象,我们通过getWindowConfigurer()来获得一个单例的WorkbenchWindowConfigurer对象。
WorkbenchWindowConfigurer getWindowConfigurer() {
        if (windowConfigurer == null) {
            // lazy initialize
            windowConfigurer = new WorkbenchWindowConfigurer(this);
        }
        return windowConfigurer;
}
注意:这里是getWindowConfigurer()方法,跟以前的getWorkbenchConfigurer()方法是不一样的。它返回一个WorkbenchWindowConfigurer对象,用来配置工作台窗体。
(11)createWorkbenchWindowAdvisor()
上面讲到,这个方法在WorkbenchAdvistor中定义的,实际上我们调用的是这个WorkbenchAdvistor子类的createWorkbenchWindowAdvisor方法。
在开篇的第2点中,在WorWorkbenchAdvisor的实现类ApplicationWorkbenchAdvisor中有这个方法中,返回了一个ApplicationWorkbenchWindowAdvisor对象。并且将WorkbenchWindowConfigurer作为参数传入。
public WorkbenchWindowAdvisor createWorkbenchWindowAdvisor(
           IWorkbenchWindowConfigurer configurer) {
       return new ApplicationWorkbenchWindowAdvisor(configurer);
   }
下面我们再看看(8)中fireWindowOpening()方法的preWindowOpen()方法。
(12)preWindowOpen()方法
这个方法是在WorkbenchWindowAdvistor类中定义的。在有关工作台生命周期的文章中提到:preWindowOpen方法是在每个窗体打开之前来调用这个方法。比如说,我们可以在这个方法中定义窗体的名称,状态栏等等。但我们需要在WorkbenchWindowAdvistor的子类中实现这个方法。
在3.1的版本中,preWindowOpen()实际上调用的是WorkbenchWindowAdvisor的子类ApplicationWorkbenchWindowAdvisor的preWindowOpen(),这个类是由我们自己定义的,重载了这个方法,用来配置窗体的信息。
在这个方法中,可以通过getWindowConfigurer()来获得WorkbenchWindowConfigurer对象,依靠这个对象配置窗体的信息。
将prewindowOpen方法执行完毕后,接着执行WorkbenchWindow构造方法中的fillActionBars(FILL_ALL_ACTION_BARS)方法。如第(7)所示。
(13)fillActionBars方法
首先看看这个方法的方法体,
public void fillActionBars(int flags) {
        Workbench workbench = getWorkbenchImpl();
        workbench.largeUpdateStart();
        try {
            getActionBarAdvisor().fillActionBars(flags);
        } finally {
            workbench.largeUpdateEnd();
        }
    }
这个方法中较为核心的部分是:getActionBarAdvisor().fillActionBars(flags);进入getActionBarAdvisor()方法:
ActionBarAdvisor getActionBarAdvisor() {
        if (actionBarAdvisor == null) {
            actionBarAdvisor = getWindowAdvisor().createActionBarAdvisor(
getWindowConfigurer().getActionBarConfigurer());
            Assert.isNotNull(actionBarAdvisor);
        }
        return actionBarAdvisor;
    }
这个方法作为WorkbenchWindows的私有类,其作用是返回一个ActionBarAdvisor对象。ActionBarAdvisor主要用来配置工作台窗体的aciton bar(菜单栏,工具栏)。在应用程序中,我们需要继承这个ActionBarAdvisor类,并重载一些方法以配置窗体的action bar,使我们的窗体变得美观,而适合现今流行应用程序的需要。
在工作台的生命周期中,这个方法在关键点被访问。(当然了,所有的这些访问都发生在访问PlatformUI.createAndRunWorkbench的范围之类了)
fillActionBars – 在访问了WorkbenchWindowAdvisor.preWindowOpen 之后,这个方法将被访问,以用来配置窗体的action bars。
ActionBarAdvisor的注释:
Public base class for configuring the action bars of a workbench window.
An application should declare a subclass of ActionBarAdvisor and override methods to configure a window's action bars to suit the needs of the particular application.
The following advisor methods are called at strategic points in the workbench's lifecycle (all occur within the dynamic scope of the call to PlatformUI.createAndRunWorkbench):
fillActionBars - called after WorkbenchWindowAdvisor.preWindowOpen to configure a window's action bars
通过getWindowAdvisor()获得WorkbenchWindowsAdvisor类,它是一个单例方法,详见(9)。
我们再来看看createActionBarAdvisor(getWindowConfigurer().getActionBarConfigurer())和其参数。参数是一个ActionbarConfigure对象。getWindowConfigurer()我们已经描述,详见(9)中描述。getActionBarConfigurer()也是一个单例方法,用来返回WindowActionBarConfigurer对象。
public IActionBarConfigurer getActionBarConfigurer() {
        if (actionBarConfigurer == null) {
            // lazily initialize
            actionBarConfigurer = new WindowActionBarConfigurer();
        }
        return actionBarConfigurer;
    }
createActionBarAdvisor()实际上是在WorkbenchWindowAdvisor的子类ApplicationWorkbenchWindowAdvisor中实现的:我们通过WorkbenchActionBuilder来返回不同窗体的菜单栏和工具栏。关于这个类,我们已经在RCP应用程序开发之三——如何打开多个工作台窗体一文中已经描述了。
public ActionBarAdvisor createActionBarAdvisor(
           IActionBarConfigurer configurer) {
       if (fActionBuilder == null)
       fActionBuilder = new WorkbenchActionBuilder(configurer);
       return fActionBuilder.makeWinAction();
       //return new ApplicationActionBarAdvisor(configurer);
   }
于是,我们通过fActionBuilder.makeWinAction()返回了一个ActionBarConfigurer对象。
fillActionbar(flag)则是用给定的action bar配置来配置action bars。他是在preWindowOpen()之后被调用。
至此,一个WorkbenchWindow窗体实例化成功。
(14)postStartup()方法
我们在看(7),执行了下面两条语句。通过newWindow.create();创建了窗体的shell。
newWindow.create(); // must be created before adding to window manager
windowManager.add(newWindow);
然后将newWindow添加到windowManager中。
执行下面的一句:newWindow.open();窗体基本打开了。
我们在看init方法,advisor.openWindows()返回一个true,表明窗体已经打开了。
最后,init返回true。
然后再runUI中,如果,init成功返回true,则准备执行下面的语句
if (initOK) {
   advisor.postStartup(); // may trigger a close/restart
}
这个方法在工作台窗体被打开(或恢复)之后,可以执行任何操作,但是必须在主事件循环运行之前。用户需要继承WorkbenchAdvisor这个类然后重载这个方法。可以参考工作台生命周期的描述:对该方法的调用是执行的第三个操作,它的调用是在第一个窗口打开之后,可以用该方法重新启用 preStartup 方法中临时禁用的项。
(15)runEventLoop方法
我们看看这个方法体:
private void runEventLoop(Window.IExceptionHandler handler, Display display) {
       runEventLoop = true;
       while (runEventLoop) {
           try {
               if (!display.readAndDispatch()) {
                   getAdvisor().eventLoopIdle(display);
               }
           } catch (Throwable t) {
               handler.handleException(t);
           }
       }
   }
其中,当我们没有任何事情需要处理时,可以调用eventLoopIdle()这个方法,但用户不能直接调用,需要在子类中重载这个方法,但用户不能重载一个空方法。建议在这个方法中访问Iworkbench.close()方法。
到此为止,一个工作台的窗体就完全的打开了。
     我们在看看runUI()方法的finally部分,当系统退出时,开始执行这部分代码。


注:本文转载自:http://blog.csdn.net/jdenght/archive/2006/08/19/1097736.aspx

分享到:
评论

相关推荐

    Eclipse RCP Plug-in开发自学教程(Eclipse3.6)

    - **启动RCP程序**:介绍如何运行RCP程序,并理解程序启动过程。 - **程序VS产品**:区分程序与产品的概念,产品是RCP应用程序的打包形式,包含特定的配置和依赖。 - **维护LAUNCH配置**:学习如何管理和修改程序...

    eclipse-rcp-juno-SR2-win32-x86_64.zip

    7. **模型驱动开发**:Eclipse RCP支持模型驱动的开发方式,允许开发者以一种抽象的、模型化的方式描述应用,然后自动生成相应的代码,提高开发效率。 8. **部署与发布**:Eclipse RCP应用可以通过产品配置进行打包...

    eclipse-rcp-2023-06-R-win32-x86-64.zip

    2. **工作台(Workbench)**: Eclipse RCP的核心是工作台,它管理着窗口、视图、编辑器等用户界面元素。工作台提供了标准的UI元素,如菜单、工具栏和透视图,开发者可以根据需求定制它们。 3. **透视图...

    EclipseRcp 例子程序

    2. **工作台(Workbench)**:工作台是Eclipse RCP应用程序的中心,负责管理窗口、视图、编辑器等元素。开发者可以自定义工作台布局,例如添加新的视图或编辑器。 3. **视图(View)**:视图是用户界面中显示特定...

    eclipse rcp excel表转mysql程序 源码

    总之,这个“eclipse rcp excel表转mysql程序”项目结合了Eclipse RCP的桌面应用开发能力、Apache POI的Excel处理功能以及JDBC的数据库操作,实现了从Excel数据到MySQL数据库的自动化转换。开发者可以通过学习和理解...

    Eclipse-RCP中文入门教程

    创建完RCP应用后,Eclipse会自动生成一系列的核心组件,包括但不限于: - **Application类**:这是RCP应用的入口点,负责启动工作台(Workbench)。 - **ApplicationActionBarAdvisor类**:用于定义工具栏和菜单栏...

    使用RCP进行程序开发(学习篇)

    创建一个基本的RCP程序包括以下步骤: 1. 创建一个新的插件项目,选择"富客户机应用程序支持"。 2. 选择一个模板,例如最简单的模板,方便初学者理解。 3. 修改应用程序标题。 4. 在Eclipse环境中运行项目,验证...

    RCP-publin

    2. **工作台(Workbench)**:Eclipse RCP的工作台是应用程序的中心,它管理着窗口、视图、编辑器等UI元素,并提供事件处理和资源管理。 3. **透视图(Perspective)**:透视图是用户界面的一种布局,可以根据不同...

    rcp工程的描述

    RCP的工作台是整个应用程序的中心,负责管理窗口、视图、编辑器和其他UI元素。工作台是所有用户交互的入口点,它协调各个插件的活动,确保它们正确地参与到用户的操作流程中。 6. **模型驱动开发(MDD)** RCP...

    一个用RCP写的CRM程序

    在RCP程序中,工作台(Workbench)是应用程序的中心,负责管理窗口、视图和编辑器。视图(View)展示特定的数据或功能,如客户列表或销售数据图表;编辑器(Editor)则用于编辑和查看详细信息,比如客户详情或订单...

    rcp研究总结

    创建第一个RCP程序,可以通过Eclipse的Plugin Development Environment (PDE) 插件向导生成模板项目。步骤包括新建Plug-in Project,选择“Rich Client Application”,选择模板(如Hello RCP),并运行以查看结果。...

    Rcp一个简单的入门教程

    10. **部署与发布**:最后,了解如何打包和部署RCP应用,包括生成可执行的自包含RCP应用程序,或者发布为Eclipse更新站点,以便用户通过Eclipse的软件更新功能进行安装和升级。 在"一个简单的RCP入门教程.pdf"中,...

    rcp menubarpath和toolbarpath的参考值

    在 Eclipse RCP(Rich Client Platform)应用程序中,`menubarPath` 是用来定义菜单结构的一种方式。通过指定不同的路径,可以实现菜单项的组织与分类。这有助于用户更高效地找到他们所需的菜单项。 #### 二、...

    Eclipse RCP开发详解

    **Workbench**是应用程序的主要工作区,它管理窗口、视图和编辑器。**Perspective**定义了工作区的布局,可以根据不同任务或用户需求切换。**View**显示特定类型的数据,而**Editor**则用于编辑和查看文件。**...

    Eclipse-4-RCP教程1

    通过向导,你可以快速生成一个基本的 RCP 应用框架,然后在此基础上进行定制。 7. **Eclipse 4 应用程序模型** Eclipse 4 引入了一种新的应用程序模型,它基于 EMF(Eclipse Modeling Framework),使得应用程序...

    Eclipse RCP应用系统开发方法与实战

    2. **工作台(Workbench)**:工作台是RCP应用程序的中心,它管理窗口、视图和编辑器等用户界面元素。 3. **透视图(Perspective)**:透视图定义了用户界面的布局,可以为不同的任务设置不同的视图组合。 4. **...

    RCP.rar_rcp

    1. **工作台(Workbench)**:工作台是RCP应用程序的核心,负责管理窗口、视图和编辑器。它提供了用户界面的基本布局,并处理用户交互。 2. **透视图(Perspective)**:透视图决定了窗口中显示哪些视图和编辑器,...

    RCP 中文文档

    3. **工作台(Workbench)**:RCP的核心是工作台,它管理着窗口、视图、编辑器等用户界面元素。开发者可以自定义工作台布局,以满足特定应用的需求。 4. **透视图(Perspective)**:透视图是RCP中的一个重要概念,...

Global site tag (gtag.js) - Google Analytics