之前学习的内容只能形成一个用户界面,而用户不能对其有实际的操作,也就是说用户界面没有任何功能。要能够让图形界面接收用户的操作,就必须给各个组件加上事件处理机制。在事件处理的过程中,主要涉及三类对象:

  • Event-事件,用户对界面操作在java语言上的描述,以类的形式出现,例如键盘操作对应的事件类是KeyEvent。 
  • Event Source-事件源,事件发生的场所,通常就是各个组件,例如按钮Button。
  • Event handler-事件处理者,接收事件对象并对其进行处理的对象。

  例如,如果用户用鼠标单击了按钮对象button,则该按钮button就是事件源,而java运行时系统会生成ActionEvent类的对象actionE,该对象中描述了该单击事件发生时的一些信息,然后,事件处理者对象将接收由java运行时系统传递过来的事件对象actionE并进行相应的处理。

同一个事件源上可能发生多种事件,因此java采取了授权处理机制(Delegation Model),事件源可以把在其自身所有可能发生的事件分别授权给不同的事件处理者来处理。

有时也将事件处理者称为监听器,主要原因也在于监听器时刻监听着事件源上所有发生的事件类型,一旦该事件类型与自己所负责处理的事件类型一致,就马上进行处理。授权模型把事件的处理委托给外部的处理实体进行处理,实现了将事件源和监听器分开的机制。事件处理者(监听器)通常是一个类,该类如果要能够处理某种类型的事件,就必须实现与该事件类型相对的接口。例如类ButtonHandler之所以能够处理ActionEvent事件,原因在于它实现了与ActionEvent事件对应的接口ActionListener。每个事件类都有一个与之相对应的接口。

使用授权处理模型进行事件处理的一般方法归纳如下:

  1.对于某种类型的事件XXXEvent, 要想接收并处理这类事件,必须定义相应的事件监听器类,该类需要实现与该事件相对应的接口XXXListener;

具体的实现方法又有4种。

  2.事件源实例化以后,必须进行授权,注册该类事件的监听器,使用addXXXListener(XXXListener ) 方法来注册监听器。

  • 事件类

与AWT有关的所有事件类都由java.awt.AWTEvent类派生,它也是EventObject类的子类。AWT事件共有10类,可以归为两大类:低级事件和高级事件。

  java.util.EventObject类是所有事件对象的基础父类,所有事件都是由它派生出来的。AWT的相关事件继承于java.awt.AWTEvent类,这些AWT事件分为两大类:低级事件和高级事件,低级事件是指基于组件和容器的事件,当一个组件上发生事件,如:鼠标的进入,点击,拖放等,或组件的窗口开关等,触发了组件事件。高级事件是基于语义的事件,它可以不和特定的动作相关联,而依赖于触发此事件的类,如在TextField中按Enter键会触发ActionEvent事件,滑动滚动条会触发AdjustmentEvent事件,或是选中项目列表的某一条就会触发ItemEvent事件。

低级事件
  ComponentEvent( 组件事件:组件尺寸的变化,移动)
  ContainerEvent( 容器事件:组件增加,移动)
  WindowEvent( 窗口事件:关闭窗口,窗口闭合,图标化)
  FocusEvent( 焦点事件:焦点的获得和丢失)
  KeyEvent( 键盘事件:键按下、释放)
  MouseEvent( 鼠标事件:鼠标单击,移动)

高级事件(语义事件)
  ActionEvent(动作事件:按钮按下,TextField中按Enter键)
  AdjustmentEvent(调节事件:在滚动条上移动滑块以调节数值)
  ItemEvent(项目事件:选择项目,不选择"项目改变")
  TextEvent(文本事件,文本对象改变)

  • 事件监听器

每类事件都有对应的事件监听器,监听器是接口,根据动作来定义方法。

  例如,与键盘事件KeyEvent相对应的接口是:
  public interface KeyListener extends EventListener {
     public void keyPressed(KeyEvent ev);
     public void keyReleased(KeyEvent ev);
     public void keyTyped(KeyEvent ev);
  }

  注意到在本接口中有三个方法,那么java运行时系统何时调用哪个方法?其实根据这三个方法的方法名就能够知道应该是什么时候调用哪个方法执行了。当键盘刚按下去时,将调用keyPressed( )方法执行,当键盘抬起来时,将调用keyReleased( )方法执行,当键盘敲击一次时,将调用keyTyped( )方法执行。

AWT的组件类中提供注册和注销监听器的方法:

  注册监听器:
  public void add<ListenerType> (<ListenerType>listener);

  注销监听器:
  public void remove<ListenerType> (<ListenerType>listener);

  例如Button类:
  public class Button extends Component {
     ……
     public synchronized void addActionListener(ActionListener l);
     public synchronized void removeActionListener(ActionListener l);
     ……}

AWT事件及其相应的监听器接口

事件类别
描述信息
接口名
方法
 ActionEvent 激活组件   ActionListener  actionPerformed(ActionEvent)
 ItemEvent 选择了某些项目   ItemListener  itemStateChanged(ItemEvent)
 MouseEvent 鼠标移动   MouseMotionListener  mouseDragged(MouseEvent)
 mouseMoved(MouseEvent)
鼠标点击等   MouseListener  mousePressed(MouseEvent)
 mouseReleased(MouseEvent)
 mouseEntered(MouseEvent)
 mouseExited(MouseEvent)
 mouseClicked(MouseEvent)
 KeyEvent 键盘输入   KeyListener  keyPressed(KeyEvent)
 keyReleased(KeyEvent)
 keyTyped(KeyEvent)
 FocusEvent 组件收到或失去焦点   FocusListener  focusGained(FocusEvent)
 focusLost(FocusEvent)
 AdjustmentEvent 移动了滚动条等组件   AdjustmentListener  adjustmentValueChanged(AdjustmentEvent)
 ComponentEvent 对象移动缩放显示隐藏等   ComponentListener  componentMoved(ComponentEvent)
 componentHidden(ComponentEvent)
 componentResized(ComponentEvent)
 componentShown(ComponentEvent)
 WindowEvent 窗口收到窗口级事件   WindowListener  windowClosing(WindowEvent)
 windowOpened(WindowEvent)
 windowIconified(WindowEvent)
 windowDeiconified(WindowEvent)
 windowClosed(WindowEvent)
 windowActivated(WindowEvent)
 windowDeactivated(WindowEvent)
 ContainerEvent 容器中增加删除了组件   ContainerListener  componentAdded(ContainerEvent)
 componentRemoved(ContainerEvent)
 TextEvent 文本字段或文本区发生改变   TextListener  textValueChanged(TextEvent)
 
  • 事件适配器

      为什么要事件适配器

一个组件可以注册多个监听器,这些监听器响应某个事件的顺序是不确定的与该监听器注册的顺序没有关系。但是监听器类去实现其对应的接口时必须要实现其所有方法,但某些方法是用不到的。

WindowAdapter 是一个抽象类,实现了WindowListener 的所有方法,当 组件注册了 WindowAdapter 适配器后,不需要再去实现所有的成员方法,只需要重写需要用到方法就可以了,这样可以保证代码的整洁。

Java语言为一些Listener接口提供了适配器(Adapter)类。可以通过继承事件所对应的Adapter类,重写需要方法,无关方法不用实现。事件适配器为我们提供了一种简单的实现监听器的手段, 可以缩短程序代码。但是,由于java的单一继承机制,当需要多种监听器或此类已有父类时,就无法采用事件适配器了。

1.事件适配器--EventAdapter

  java.awt.event包中定义的事件适配器类包括以下几个:
  1.ComponentAdapter( 组件适配器)
  2.ContainerAdapter( 容器适配器)
  3.FocusAdapter( 焦点适配器)
  4.KeyAdapter( 键盘适配器)
  5.MouseAdapter( 鼠标适配器)
  6.MouseMotionAdapter( 鼠标运动适配器)
  7.WindowAdapter( 窗口适配器)

 2. 用内部类实现事件处理

  内部类(inner class)是被定义于另一个类中的类,使用内部类的主要原因是由于:

  • 一个内部类的对象可访问外部类的成员方法和变量,包括私有的成员。  
  • 实现事件监听器时,采用内部类、匿名类编程非常容易实现其功能。  
  • 编写事件驱动程序,内部类很方便。    因此内部类所能够应用的地方往往是在AWT的事件处理机制中。

3.匿名类(Anonymous Class)

  当一个内部类的类声名只是在创建此类对象时用了一次,而且要产生的新类需继承于一个已有的父类或实现一个接口,才能考虑用匿名类,由于匿名类本身无名,因此它也就不存在构造方法,它需要显示地调用一个无参的父类的构造方法,并且重写父类的方法。所谓的匿名就是该类连名字都没有,只是显示地调用一个无参的父类的构造方法。

  • 事件类具体的实现方法有以下几种:

    (1)内部类实现监听器接口

    • 实现代码
    public class Demo01 {
    
        public static void main(String[] args) {
    final Frame frame = new Frame("事件");
    frame.setBounds(100, 100, 300, 180);
    frame.setVisible(true); //匿名内部类 frame.addWindowListener(new WindowListener (){ @Override
    public void windowActivated(WindowEvent e) {
    // TODO Auto-generated method stub } @Override public void windowClosed(WindowEvent e) {
    // TODO Auto-generated method stub } @Override
    //进行窗口的关闭时被调用
    public void windowClosing(WindowEvent e) {
    //在匿名类内部访问外部变量,则该变量一定是 final
    System.out.println("closeing"); frame.dispose();//关闭当前窗口 } @Override
    public void windowDeactivated(WindowEvent e) {
    // TODO Auto-generated method stub } @Override
    public void windowDeiconified(WindowEvent e) {
    // TODO Auto-generated method stub } @Override
    public void windowIconified(WindowEvent e) {
    // TODO Auto-generated method stub } @Override
    public void windowOpened(WindowEvent e) {
    // TODO Auto-generated method stub } }); } }

    要关闭窗口,则要用到 WindowEvent 类,并且去实现 WindowListener 接口,然后创建WindowListener的对象,再用 frame 去调用 addWindowListener()方法,该对象作为参数,即注册了一个事件监听器。

    在匿名内部类的成员方法中 用 frame 调用dispose() 方法来关闭当前窗口。

    关闭按钮为一个事件源,当用户点击关闭按钮后,会触发 closeing 这一事件,此时监听器会捕获这一事件,当监听器(接口)捕获到 closeing 事件时,会去调用它的 closing 方法。程序会执行关闭 frame 这一方法来关闭当前窗口。

    (2)容器类(外部类)实现监听器接口

    • 实现代码
    public class Demo02 implements WindowListener{
    //定义 Frame 类的成员变量
    public Frame frame ;
    //构造函数
    public Demo02(Frame frame){ this.frame = frame;
    }
    public static void main(String[] args) {
    final Frame frame = new Frame("事件");
    //实例化监听器(容器)类,并将 frame 对象作为参数传递过去
    Demo02 d = new Demo02(frame);
    //注册监听器
    frame.addWindowListener(d); frame.setBounds(100, 100, 400, 280);
    frame.setVisible(true);
    }
    @Override
    public void windowActivated(WindowEvent e) {
    // 不需要的方法体为空
    }
    @Override
    public void windowClosed(WindowEvent e) {
    // TODO Auto-generated method stub }
    @Override
    public void windowClosing(WindowEvent e) {
    frame.dispose();
    }
    @Override
    public void windowDeactivated(WindowEvent e) {
    // TODO Auto-generated method stub }
    @Override
    public void windowDeiconified(WindowEvent e) {
    // TODO Auto-generated method stub }
    @Override
    public void windowIconified(WindowEvent e) {
    // TODO Auto-generated method stub }
    @Override
    public void windowOpened(WindowEvent e) {
    // TODO Auto-generated method stub }
    }

    使用 1 个容器类也可以实现多个监听器接口,但要注意的是在AWT中就经常用到声明和实现多个接口。记住无论实现了几个接口,接口中已定义的方法必须一一实现,如果对某事件不感兴趣,可以不具体实现其方法,而用空的方法体来代替。但却必须所有方法都要写上。

    (3)自定义子类实现内部接口

    • 代码实现
    public class MyWindowListener implements WindowListener{
    public Frame frame;
    //以frame 对象作为参数的构造方法
    public MyWindowListener(Frame frame){
    this.frame = frame;
    } @Override
    public void windowActivated(WindowEvent e) {
    // TODO Auto-generated method stub }
    @Override
    public void windowClosed(WindowEvent e) {
    // TODO Auto-generated method stub
    }
    @Override
    public void windowClosing(WindowEvent e) {
    frame.dispose();
    } @Override
    public void windowDeactivated(WindowEvent e) {
    // TODO Auto-generated method stub } @Override
    public void windowDeiconified(WindowEvent e) {
    // TODO Auto-generated method stub }
    @Override
    public void windowIconified(WindowEvent e) {
    // TODO Auto-generated method stub
    }
    @Override
    public void windowOpened(WindowEvent e) { }
    }

    因为在前面编写的第一个程序中已经进行了 Frame 类的实例化,所以此段代码中只做声明就可以了,运行时系统会自动访问前边的frame对象完成操作,不需要进行实例化以及 设置frame 用frame 对象去注册监听器等操作。 实质上以上第二种和第三种方法可以视为相同。

    (4)采用适配器

    • 代码实现
    public class Demo03 {
    
        public static void main(String[] args) {
    final Frame frame = new Frame("事件");
    frame.setBounds(100, 100, 300, 180);
    frame.setVisible(true);
    //匿名内部类
    frame.addWindowListener(new WindowAdapter(){
    //重写需要的方法
    @Override
    public void windowClosing(WindowEvent arg0) { frame.dispose();
    } });
    }
    }

    WindowAdapter 是一个抽象类,实现了WindowListener 的所有方法(空实现),当 组件注册了 WindowAdapter 适配器后,不需要再去实现所有的成员方法,只需要重写需要用到方法就可以了,这样可以保证代码的整洁。

  • AWT组件库(1)

1. 按钮(Button)

  按钮是最常用的一个组件,其构造方法是:Button b = new Button("Quit");
  当按钮被点击后,会产生ActionEvent事件,需ActionListener接口进行监听和处理事件。
  ActionEvent的对象调用getActionCommand()方法可以得到按钮的标识名,缺省按钮名为label。
  用setActionCommand()可以为按钮设置组件标识符。

2.复选框 (Checkbox)

  复选框提供简单的"on/off"开关,旁边显示文本标签。
  
  构造方法如下:
  setLayout(new GridLayout(3,1));
  add(new Checkbox("one",null,true));
  add(new Checkbox("two"));
  add(new Checkbox("three"));
  复选框用ItemListener 来监听ItemEvent事件,当复选框状态改变时用getStateChange()获取当前状态。使用getItem()获得被修改复选框的字符串对象。

3.复选框组(CheckboxGroup)

  使用复选框组,可以实现单选框的功能。方法如下:
  setLayout(new GridLayout(3, 1));
  CheckboxGroup cbg = new CheckboxGroup();
  add(new Checkbox("one", cbg, true));
  add(new Checkbox("two", cbg, false));
  add(new Checkbox("three", cbg, false));

  • AWT组件库(2)

4. 下拉式菜单(Choice)

  下拉式菜单每次只能选择其中的一项,它能够节省显示空间,适用于大量选项。
  Choice Colorchooser=new Choice();
  Colorchooser.add("Green");
  Colorchooser.add("Red");
  Colorchooser.add("Blue");
  Choice 用ItemListener接口来进行监听

 5. Canvas

  一个应用程序必须继承Canvas类才能获得有用的功能,比如创建一个自定义组件。如果想在画布上完成一些图形处理,则Canvas类中的paint()方法必须被重写。
  Canvas组件监听各种鼠标,键盘事件。当在Canvas组件中输入字符时,必须先调用requestFocus()方法。

6. 单行文本输入区(TextField)

  只能显示一行,当回车键被按下时,会发生ActionEvent事件,可以通过ActionListener中的actionPerformed()方法对事件进行相应处理。可以使用setEditable(boolean)方法设置为只读属性。

  单行文本输入区构造方法如下:
  TextField tf1,tf2,tf3,tf4:
  tf1=new TextField();
  tf2=new TextField("",20); //显示区域为20列
  tf3=new TextField("Hello!"); //按文本区域大小显示
  tf4=new TextField("Hello!",30); //初始文本为Hello!, 显示区域为30列

  • AWT组件库(3)

7. 文本输入区(TextArea)

  TextArea可以显示多行多列的文本。使用setEditable(boolean)方法,可以将其设置为只读的。在TextArea中可以显示水平或垂直的滚动条。
要判断文本是否输入完毕,可以在TextArea旁边设置一个按钮,通过按钮点击产生的ActionEvent对输入的文本进行处理。

8. 列表(List)

  列表中提供了多个文本选项,列表支持滚动条,可以浏览多项
  List lst=new List(4,false); //两个参数分别表示显示的行数、是否允许多选
  lst.add("Venus");
  lst.add("Earth");
  lst.add("JavaSoft");
  lst.add("Mars");
  cnt.add(lst);

 9. 框架(Frame)

  Frame是顶级窗口,可以显示标题,重置大小。当Frame被关闭,将产生WindowEvent事件,Frame无法直接监听键盘输入事件。

 10. 对话框(Dialog)

  它是Window类的子类。对话框和一般窗口的区别在于它依赖于其它窗口。对话框分为非模式(non-modal)和模式(modal)两种。

 11. 文件对话框(Filedialog)

  当用户想打开或存储文件时,使用文件对话框进行操作。主要代码如下:
  FileDialog d=new FileDialog(ParentFr,"FileDialog");
  d.setVisible(true);
  String filename=d.getFile();

 12. 菜单(Menu)

  无法直接将菜单添加到容器的某一位置,也无法使用布局管理器对其加以控制。菜单只能被添加?quot;菜单容器"(MenuBar)中。

 13. MenuBar

  只能被添加到Frame对象中,作为整个菜单树的根基。
  Frame fr = new Frame("MenuBar");
  MenuBar mb = new MenuBar();
  fr.setMenuBar(mb);
  fr.setSize(150,100);
  fr.setVisible(true);

 14. Menu

  下拉菜单。它可以被添加到MenuBar中或其它Menu中。
  Frame fr = new Frame("MenuBar");
  MenuBar mb = new MenuBar();
  fr.setMenuBar(mb);
  Menu m1 = new Menu("File");
  Menu m2 = new Menu("Edit");
  Menu m3 = new Menu("Help");
  mb.add(m1);
  mb.add(m2);
  mb.setHelpMenu(m3);
  fr.setSize(200,200);
  fr.setVisible(true);
   

 15. MenuItem

  MenuItem是菜单树中的"叶子节点"。MenuItem通常被添加到一个Menu中。对于MenuItem对象可以添加ActionListener,使其能够完成相应的操作。
  Menu m1 = new Menu("File");
  MenuItem mi1 = new MenuItem("Save");
  MenuItem mi2 = new MenuItem("Load");
  MenuItem mi3 = new MenuItem("Quit");

  m1.add(mi1);
  m1.add(mi2);
  m1.addSeparator();
  m1.add(mi3);

  • 总结

组件是各种各样的类,封装了图形系统的许多最小单位,例如按钮、窗口等等;而容器也是组件,它的最主要的作用是装载其它组件,但是象Panel这样的容器也经常被当作组件添加到其它容器中,以便完成杂的界面设计。布局管理器是java语言与其它编程语言在图形系统方面较为显著的区别,容器中各个组件的位置是由布局管理器来决定的,共有5种布局管理器,每种布局管理器都有自己的放置规律。事件处理机制能够让图形界面响应用户的操作,主要涉及到事件源、事件、事件处理者等三方,事件源就是图形界面上的组件,事件就是对用户操作的描述,而事件处理者是处理事件的类。因此,对于AWT中所提供的各个组件,我们都需要了解该组件经常发生的事件以及处理该事件的相应的监听器接口。

AWT初步— 事件处理模型的更多相关文章

  1. Java NIO 与 基于reactor设计模式的事件处理模型

    Java NIO非堵塞应用通常适用用在I/O读写等方面,我们知道,系统运行的性能瓶颈通常在I/O读写,包括对端口和文件的操作上,过去,在打开一个I/O通道后,read()将一直等待在端口一边读取字节内 ...

  2. AWT/Swing——事件处理

    前言 当用户点击图形界面上的一个按钮或者其他Component时要有所响应,这才是实现了图形界面的交互功能.如何做出这些响应我们就需要了解事件的处理机制.下面将分为以下内容介绍AWT(Swing)中事 ...

  3. AWT初步—Frame和 Panel

    初识 AWT       GUI 和 AWT GUI:Graphics User Interface  图形用户界面 AWT:Abstract Window Toolkit  抽象窗口工具集 之前的程 ...

  4. Swing与AWT在事件模型处理上是一致的。

    Swing与AWT在事件模型处理上是一致的. Jframe实际上是一堆窗体的叠加. Swing比AWT更加复杂且灵活. 在JDK1.4中,给JFRAME添加Button不可用jf.add(b).而是使 ...

  5. Swing与AWT在事件模型处理上是一致的

    Swing与AWT在事件模型处理上是一致的. Jframe实际上是一堆窗体的叠加. Swing比AWT更加复杂且灵活. 在JDK1.4中,给JFRAME添加Button不可用jf.add(b).而是使 ...

  6. javascript——事件处理模型(DOM 和 IE)

    javascript的事件处理模型分为 DOM事件处理模型和 IE事件处理模型. 一.DOM事件流模型 DOM事件流分为三个阶段:捕获阶段.目标阶段.冒泡阶段. 捕获阶段:自上而下,由document ...

  7. Nginx学习笔记(一):Nginx 进程模型 / 事件处理模型

    Nginx 进程模型 ​​​​ 多进程模型 进程间相互独立,无需加锁,且互不影响: 一个进程退出了不影响其他的进程运行,降低风险: 当请求到来,多个 worker 通过竞争 accrpt_mutex ...

  8. JavaScript事件:事件处理模型(冒泡、捕获)、取消冒泡、阻止默认事件

    (一)事件处理模型---事件冒泡.捕获 (1)事件冒泡 24 <body> 25 <div class="warpper"> 26 <div clas ...

  9. I/O事件处理模型之Reactor和Proactor 【转】

    http://blog.ddup.us/?p=280 这篇博客说的很清楚,赞一个: 在编写服务端软件的时候,如何处理各种I/O事件是其中很重要的一部分.在Unix Network Programmin ...

随机推荐

  1. Java基础--枚举

    1.枚举简介 枚举是由一组固定的常量组成的类型,自定义数据类型. 枚举的常量值一定是可列举的有限值.常量值的类型都是public static final. 下面代码中的Gender 是一种自定义的数 ...

  2. Maven - 实例-4-依赖传递

    这里以Eclipse创建Maven工程来演示. Setp-1 创建Maven项目 File ---> New ---> Maven Project ---> 默认勾选"Us ...

  3. .NET手记-Autofac进阶(属性和方法注入 Property and Method Injection)

    尽管构造函数参数注入是传递参数值给当前构造的组件的优先方式,但是你也可以使用属性或者方法注入来提供参数值. 属性注入使用可写入的变量而不是构造函数参数来完成注入.方法注入则通过方法来设置依赖项. 属性 ...

  4. hive中的子查询改join操作(转)

    这些子查询在oracle和mysql等数据库中都能执行,但是在hive中却不支持,但是我们可以把这些查询语句改为join操作: -- 1.子查询 select * from A a where a.u ...

  5. Retrieving the COM class factory for component with CLSID {00024500-0000-0000-C000-000000000046} failed due to the following error: 80070005 拒绝访问

    异常信息:Retrieving the COM class factory for component with CLSID {00024500-0000-0000-C000-000000000046 ...

  6. 2018.4.24-ml笔记(多元线性回归)

    numpy.dot作用于两个向量则是它们内积,作用于矩阵则是矩阵积. RMSE解决量纲问题,即单位 RMSE会放大差值比较大的值,所以选用MSE更好.

  7. (原创)UML要点总结

    今天我们总结要点: 我们就从这张图慢慢讲. 一.类图部分 基础: 类图→长方形表示.类名在最上栏,下面是数据,第三栏是方法.其存在两种关系:关联和泛化 属性: 全形:  可见性  名:类型     重 ...

  8. 从零开始学 Web 之 jQuery(七)事件冒泡,事件参数对象,链式编程原理

    大家好,这里是「 从零开始学 Web 系列教程 」,并在下列地址同步更新...... github:https://github.com/Daotin/Web 微信公众号:Web前端之巅 博客园:ht ...

  9. Lombok(1.14.8)的简单示例

    分享自: http://blog.csdn.net/huey2672/article/details/42240985 Lombok是一种Java™实用工具,可用来帮助开发人员消除Java的冗长,尤其 ...

  10. wepack---预打包dll

    一.前言 今天被问到,怎么实现webpack快速打包?话说距离上次手动配置webpack已经过去很长时间了,现在webpack都出到4.0版本了,号称零配置,还没来得及好好感受一下. ‘不就是公共模块 ...