构建可重用的 Java 组件

作者:R.J. Celestino

不可避免的是,如果您使用 Java 编程,最终您将需要一个专门的组件。我知道,Sun Microsystems 在每次 Java 下载中都提供了抽象窗口工具包 (AWT);然而,AWT 的小组件集和相当低级别的图形最终会让您感到不足。当需要给老板、客户或狗留下深刻印象时,就该卷起袖子自己动手了。

本文探讨了一些扩展 AWT 的良好技术。您不仅会学习如何创建有趣的组件。您还将(也许更重要的是)学习如何通过采用良好的设计实践并遵守 AWT 的架构来使它们可重用。为此,我将介绍 AWT 事件模型、层次结构和图形的一些重要概念。[从 1.0x 版本到 1.1x 版本,最显著的变化也许是从基于继承的事件模型转变为更强大的基于委托的模型。] 本文将只讨论 Java 1.1x 及以上版本,包括新的事件模型。

我撰写本文的期望是您具有一些 Java 的基本经验。您需要知道如何编译和运行 Java 代码来尝试这些示例。如果您已经创建了强制性的“Hello World”小程序并熟悉布局管理器,那将是一个额外的奖励。

本文中没有任何特定于 Linux 的内容。Java 的主要优势之一是它可以在任何移植了 Java 虚拟机的平台上运行。毫不奇怪,Linux 就是其中之一。事实上,您的 Linux 版本很可能已将 Java 编译到内核中。我已经在各种版本的 Linux 和 Solaris 2.5 上运行了这些示例。如果您希望在 Linux 上实验构建组件,您将需要从 Blackdown 或 sunsite 获取 Java 开发工具包 (JDK) 的 Linux 端口。当然,您将能够在任何运行在 Linux 上的启用 Java 的浏览器(即 Netscape)中运行示例小程序。

它不丑,这是我的 GUI

如果您编写 GUI 仅依赖 AWT,那么它很可能很丑陋。虽然 AWT 为 GUI 开发提供了一个出色的框架,但它从未打算成为 GUI 工具包的最终方案。AWT 提供了一组“最小公分母”组件,因此如果您对简单的按钮、滑块或文本字段感兴趣,AWT 可以满足您的需求。不幸的是,现代用户界面经常需要超出这些旧式标准的 GUI 控件。

当您的设计要求或创造性愿望超出这些标准组件的功能时,您需要创建自己的组件。当您准备好创建自己的组件时,您将有多种选择。您可以创建一个非常不合作的组件,该组件与您的应用程序紧密耦合。或者,您可以选择利用面向对象编程的强大功能和 AWT 的现有架构。我提倡后者应该不足为奇。如果您同意,您将创建开放的、可重用的组件,Java 社区中的任何人都可以使用它们,无论他选择什么操作系统。

所有其他组件都在这样做

Linux 用户通常是具有个人主义精神的一群人。个性有其用武之地,但当涉及到您的组件的架构时,我提倡与大众保持一致。当我费力创建组件时,我想确保它可以一遍又一遍地轻松使用。我通过遵守 AWT 的设计来实现这一点。虽然 AWT 不是视觉上最具吸引力的组件工具包,但它确实为组件创建和交互提供了一个出色的框架。通过在这个框架的范围内工作,我们坚持 AWT 的基本架构并获得可重用的组件。

当您设计您的组件以与现有架构协作时,您可以确保它们是健壮的、可维护的,并且易于被任何 Java 开发人员(包括您自己)重用。您的组件将是“可插拔的”。任何 Java 用户都将能够简单地插入组件并像使用 AWT 中提供的任何其他标准 GUI 组件一样使用它。

可重用组件的基础

在设计和创建组件时,有几个重要的方面需要牢记。在本节中,我将讨论其中的一些方面,并逐步演变出一个组件创建的模板。

超类

为了与 AWT 协作,您的组件必须以Component作为其祖先。我并不是说您必须直接子类化 Component;事实上,在这里,您通常可以通过进一步向下继承层次结构来继承一些有用的行为(有关 Component 层次结构的一部分,请参见图 1)。

Building Reusable Java Widgets

图 1. AWT 组件层次结构的一部分

您的组件可能会在运行时绘制,或者从存储在本地或远程机器上的图像(例如 GIF)创建。Canvas 类是此类组件的不错选择。Canvas 类提供了一个用于绘图的空白区域。选择适合您组件的超类需要一些经验。明智的做法是花一些时间研究 AWT 的 Component 层次结构,然后再选择超类。

事件和委托

如果您的组件生成事件,您必须遵守 Java 的事件模型。在“早期”,Java 使用了笨拙的基于继承的事件模型。随着 Java 1.1 的推出,AWT 采用了一种完全现代的、基于委托的事件模型。概念很简单。当用户与组件交互时,它会生成事件。对象可以注册对所有或部分这些事件的兴趣。这些感兴趣的对象,称为侦听器,接收事件并采取适当的行动。此过程称为委托。侦听器由组件委托来处理它们生成的事件。

当您创建事件类时,请确保它们包含适当的信息并在响应适当的用户操作时生成。当您开始生成自己的事件时,可能会感到困惑。首先,您创建事件类。接下来,您创建一个侦听器接口。此接口定义了事件发生时将调用的侦听器的方法。最后,您创建一个事件多路广播器。事件多路广播器的任务是将事件广播给多个侦听器。听起来很复杂,但希望在一个或两个示例之后会变得清晰。图 2 是多路广播过程的图示。

Building Reusable Java Widgets

图 2. 事件多路广播器

绘图

您的组件的外观不会严格影响其可重用性。但是,组件的外观应与 GUI 的其余部分在视觉上协调一致。以下是一些需要考虑的事项,这些事项虽然不是硬性规定,但很重要。

现代组件通过阴影产生 3D 外观。阴影由假想光源创建,按照惯例,光源位于组件的左上方。当按钮处于凸起状态时,其上边框和左边框比按钮的表面更亮(参见图 3)。其下边框和右边框更暗。这种阴影使其看起来是凸起的。交换亮区和暗区,按钮将显得凹陷(参见图 4)。

Building Reusable Java Widgets

图 3. 处于凸起状态的按钮

如果您的组件被绘制或在其上绘制了区域,请记住 3D 效果使其尽可能具有吸引力。请记住,绘图发生在您类的 paint 方法中。每当您的组件需要绘制时,AWT 都会调用 paint 方法。

Building Reusable Java Widgets

图 4. 处于按下状态的按钮

您可以拥有的组件

通常有两种方法可以创建可重用的组件:组合和专门化。在继续之前,让我们花一点时间讨论每种方法。

组合 通过组合创建的组件有时称为超级组件或复合组件。这种类型的组件只是其他组件的集合,这些组件协同工作以完成专门的任务。每当您有需要多个子组件协同工作的重复性任务时,都应创建复合组件。一些示例包括订单表、文件对话框或颜色选择器。当您创建复合组件时,重要的是隐藏其所有子组件的事件,并在更高级别生成与复合组件本身的语义相适应的事件。您将在电子邮件输入组件和窗口栏组件中看到这是如何工作的。

专门化

有时需要一个与标准 AWT 组件略有不同的组件。也许您需要添加一些新的行为或外观。在这些情况下,您应该考虑通过专门化创建组件。当您通过专门化创建组件时,您将创建一个子类并添加或覆盖现有行为。继承的强大功能为您提供了超类的所有行为,因此您只需编写新代码即可。我们将讨论的一个例子是 VerticalSeparator,它是 Canvas 的子类。它覆盖了 paint 方法以实现其自身的特殊外观。vertical-separator 是这方面的一个很好的例子。折叠面板是如何使用专门化的一个更微妙的例子。

组件示例

为了示例的清晰起见,我将图形保持在最低限度,并专注于创建可重用组件的“螺母和螺栓”。

关于样式的说明:我所有的实例变量都以一个下划线开头。大写变量是类变量(静态变量),小写变量通常是已传递到方法中或在当前块中定义的临时变量。类名和接口名始终以大写字母开头。事件类始终使用“Event”作为后缀,侦听器接口始终使用“Listener”作为后缀。

示例 1. 垂直分隔符

第一个组件是一个简单的垂直分隔符。HTML 用户应该熟悉分隔符;<hr> 标签创建水平线。分隔符用于分隔组件组。垂直分隔符是一个非常简单的组件。它不需要与任何其他组件或对象交互。它只是一个视觉组件。经过一番思考,我得出结论,应该通过专门化来创建此组件,特别是通过子类化 Canvas 类。Canvas 在这里是一个不错的选择,因为垂直分隔符的唯一职责是在屏幕上渲染自身。分隔符通常具有蚀刻的外观,就好像它们被雕刻到屏幕上一样。

1. 渲染蚀刻线

为了创建蚀刻的 3D 效果,绘制两条彼此相邻的线,一条比背景更暗,另一条更亮。这是一个简单的 Java 代码片段,用于创建两条垂直线——一条将显示为蚀刻线,另一条将显示为凸起线。有关这两条线的图片,请参见图 5。

public void paint( Graphics g ) {
        // draw a raised line
        g.setColor( _light ) ;
        g.drawLine( 5, 10, 5, 40 ) ;
        g.setColor( _dark ) ;
        g.drawLine( 6, 10, 6, 40 ) ;
        // draw an etched line
        g.setColor( _dark ) ;
        g.drawLine( 25, 10, 25, 40) ;
        g.setColor( _light ) ;
        g.drawLine( 26, 10, 26, 40) ;
        }
Building Reusable Java Widgets

图 5. 蚀刻线和凸起线

以下是我如何设置两个实例变量 _light_dark 的值

_light = getBackground().brighter().brighter() ;
_dark  = getBackground().darker().darker() ;

相对于背景颜色而不是硬编码颜色设置值使代码更通用。无论它们出现的区域的背景颜色如何,这些线都将显示为蚀刻线和凸起线。

1.2. 调整分隔符大小

垂直分隔符应垂直填充其分配的空间,并在其空间中水平居中。在 paint 方法中,您可以确定已分配的空间并计算其尺寸。以下是我如何做的

size = size() ;
int length = size.height ;
int yPosition = ( size.width )/2 ;
g.setColor( dark ) ;
g.drawLine( 0, yPosition, length, yPosition ) ;
g.setColor( light ) ;
g.drawLine( 0, yPosition+1, length, yPosition+1 );

现在,我们还需要覆盖另一个关键方法。记住,我已选择子类化 Canvas。Canvas 类提供 0x0 的默认大小。这意味着如果使用其默认大小布局组件,它将不会显示出来。为了获得有意义的默认大小,您需要覆盖 getPrefferedSizegetMinimumSize 方法。我为它的首选大小和最小大小都选择了 4x8 像素的区域。我为什么选择 4x8?分隔符的实际宽度为 2 像素。将其首选宽度设置为 4 会使其两侧各有一个 1 像素的缓冲区。8 像素的首选高度有些随意——任何大于 0 的值都是可以接受的,只要它是可见的。请记住,如果分隔符使用得当,布局管理器将将其高度增长到适当的值,而不管首选高度如何。

您可以在列表 1中看到完整的 VerticalSeparator 类。请记住,我们构建垂直分隔符是为了填充分配的垂直空间,因此请务必将其放置在适当的位置。边界布局的东部和西部部分保证是垂直区域。如果分隔符放置在这些区域中的任何一个区域中,它将增长以填充垂直区域。如果您将其放置在北部或南部区域,它的大小将被调整为其 8 像素的首选高度,这可能不是您想要的。我建议您阅读有关布局管理器以及它们如何响应组件的大小调整需求的信息。有些完全无视首选大小。您可以在图 6 中看到使用垂直分隔符的小程序。

Building Reusable Java Widgets

图 6. 运行中的 VerticalSeparator

在此示例中,我介绍了一些关于绘图和大小调整的基本概念。垂直组件的目标只是为了使您的 GUI 看起来更好。这是一个崇高的目标,但在接下来的几个示例中,我们将研究一些更努力工作的组件,它们真的物有所值。在查看这些组件之前,这里有一些挑战。

1.3. 读者练习

您可以对此类进行一些有趣的扩展,我将它们留给您

  • 通用分隔符:要创建水平分隔符,您可能需要创建一个 HorizontalSeparator 类。但是,为什么不考虑创建一个可以根据其放置位置执行两种操作的类呢?它需要知道它是否放置在垂直或水平区域中,并相应地渲染自身。

  • 附加功能:我们创建的分隔符具有 2 像素的固定宽度并且是蚀刻的。更改或扩展您的类以支持可变宽度以及蚀刻、凸起或平坦的选项。

示例 2. 电子邮件输入组件

电子邮件输入组件表示用于从用户收集信息的简单输入表单。它是一个使用标准 AWT 组件构建的复合组件。此示例组件在其与其他类交互的方式中很重要。此类广播自定义事件,这些事件可以被系统中任何感兴趣的对象捕获。将自定义事件广播给这些事件的侦听器的能力对于创建可重用组件至关重要。电子邮件组件将具有一个完成按钮和一个取消按钮。请记住,您不希望其他对象直接访问这些按钮或它们生成的事件。为什么?假设电子邮件组件在稍后发生更改并且不再使用按钮单击来表示完成。每个使用该类的对象都会中断,并且必须重写。如果您隐藏这些事件,您的组件如何将状态更改通知给感兴趣的各方?您必须生成特定于组件的事件并创建这些事件的侦听器。这些事件必须在语义上对电子邮件组件的操作有意义。我们稍后将看到这是如何完成的,但首先让我们创建它的视觉外观。

2.1. 布局

电子邮件输入表单的视觉布局在其构造函数中创建。该组件由一个文本字段、一个标签和两个按钮组成。

Class EmailEntry extends Panel implements
        ActionListener {
public EmailEntry() {
        super() ;
        _doneButton = new Button( "Done" );
        _cancelButton = new Button( "Cancel" ) ;
        _emailField = new TextField( 40 ) ;
        // build a sub-panel for the buttons.
        Panel = new Panel() ;
        buttonPanel.add( _doneButton ) ;
        buttonPanel.add( _cancelButton ) ;
        // install the components in the widget
        this.setLayout( new BorderLayout() ) ;
        this.add( "West", new Label(
                "Enter your e-mail address"));
        this.add( "Center", _emailField) ;
        this.add( "South", buttonPanel ) ;
        // forward events to myself
        _doneButton.addActionListener( this ) ;
        _cancelButton.addActionListener( this )
        }
}

请注意,在组件子类 Panel 中,我选择 panel 作为超类,以便我可以继承其布局功能。另请注意,该类实现了 ActionListener 接口。这意味着该类被允许侦听操作事件(按钮生成的事件)。在构造函数的末尾,该类将自身注册(在调用 addActionListener 中)为两个按钮的侦听器。

2.2. 创建事件类

您必须确定您的组件将生成的事件。我已选择创建一个单一的事件类,EmailEntryEvent,它可以表示“用户已完成”或“用户已取消”状态。当您创建事件类时,请记住它必须维护足够的信息才能对事件采取行动。在本例中,事件必须存储输入的电子邮件地址。列表 2 显示了 EmailEntryEvent 类。请注意,我创建了两个构造函数。当构造函数传递一个电子邮件字符串时,将创建一个类型为 done 的电子邮件输入事件。如果未将任何信息传递给构造函数,则会创建一个类型为 cancel 的事件。电子邮件输入组件负责调用正确的构造函数(对组件的合理请求)。

2.3. 创建侦听器接口

当事件广播到侦听器时,将调用侦听器类的特定方法。您可以将这些方法视为“回调”。侦听器接口定义了这些方法。该接口确保任何旨在成为侦听器的类都具有处理事件所需的方法。列表 3 显示了电子邮件输入侦听器接口。任何希望成为 EmailEntryEvents 侦听器的类都必须实现 done 方法和 cancel 方法。

2.4. 事件多路广播器

事件多路广播器处理事件到侦听器的异步广播。您会很高兴知道您不必从头开始编写自己的多路广播器。相反,您需要创建现有 AWTEventMulticaster 的子类,并编写一些方法以便它可以处理您的新事件。当您创建多路广播器时,请按照以下步骤操作

  • 实现您在列表 3 中创建的侦听器接口。

  • 使用相同的侦听器创建 addremove 方法。

  • 创建在侦听器接口中定义的方法。这些方法只是将消息转发到适当的侦听器。

查看列表 4 以查看我是如何为此示例创建多路广播器的。不要让多路广播器吓到您。代码非常基础,cookie-cutter 风格。您将依靠超类来完成所有繁重的多路广播工作。您的多路广播器必须做的就是了解您的新事件和侦听器。

2.5. 将所有内容连接起来

我们现在已经创建了所有必要的组件,剩下的就是正确连接它们。首要任务是完成 EmailWidget 类。当我们离开它时,它只有一个构造函数。接下来,您必须使其具有添加侦听器的能力。这是 addEmailEntryEventListener 方法

public void addEmailEntryListener(
        EmailEntryListener e ) {
_emailEntryListener = MyMulticaster.add(
        _emailEntryListener, e ) ;
}

请注意,该组件具有一个维护其侦听器的实例变量。如果您仔细观察,您会发现此变量实际上是您的多路广播器类的一个实例。任何组件都可能具有许多侦听器。您的多路广播器将维护侦听器列表并确保它们获得其适当的事件。最后,您必须处理内部事件(来自按钮)并生成您的新事件。

public void actionPerformed( ActionEvent e ) {
        EmailEntryEvent newEvent ;
        if ( _emailEntryListener == null ) return ;
        if ( e.getSource() == _doneButton ) {
                newEvent = new EmailEntryEvent(
                                getEmailAddress()) ;
        _emailEntryListener.done( newEvent );
    }
        else if ( e.getSource() == _cancelButton ) {
                newEvent = new EmailEntryEvent() ;
                _emailEntryListener.cancel( newEvent );
    }
在此代码中,您生成特定于组件的事件。当按下 done 按钮时,将创建一个“done”事件,该事件存储电子邮件地址。否则,将生成“cancel”事件。另请注意事件是如何分派的:在多路广播器上调用相应的方法(_emailEntryListenerMyMulticaster 的一个实例)。然后,多路广播器将方法调用转发给所有注册的侦听器。EmailEntry 组件现在已准备好被任何 Java 程序使用。列表 5 显示了一个使用电子邮件输入组件并拦截事件的简单小程序。查看图 7 中的电子邮件输入组件。
Building Reusable Java Widgets

图 7. 用作小程序的 EmailEntryWidget

一个重要的注意事项:电子邮件输入类生成一个 done 事件。这与 done 按钮生成的操作事件有何不同?差异很微妙,但很重要。如果您依赖于检测特定按钮的事件,则其他类需要了解您的类的内部工作原理的详细信息。如果这发生更改,则每个使用它的类也必须更改。

2.6. 读者练习

以下是一些扩展 EmailEntry 组件的想法,您可能会喜欢尝试

  • 收集更多信息:通过从用户收集更多信息来扩展组件。添加复选框、其他文本字段等。请记住,您的 事件类 必须维护此信息并将其传递给侦听器。

  • 添加错误检查:检查电子邮件地址是否完整和合理等内容。确保仅在表单正确填写后才广播事件。

示例 3. 窗口栏

窗口栏是折叠面板示例所需的组件,如下所示。单击窗口栏,面板折叠。再次单击它,它会打开。为了简单起见,我将使用按钮作为栏,而不是创建花哨的可视化栏。栏必须广播事件以指示面板应折叠或恢复。

3.1. 布局

此组件的布局是有意为之的微不足道,以保持示例简单。

public WindowBar() {
        super() ;
        _closer = new Button( "Collapse" ) ;
        _closer.addActionListener( this ) ;
        add( _closer ) ;
}
3.2. 创建事件类

对于此组件,我们需要一个具有“折叠”和“恢复”状态的事件。这将与我们的电子邮件输入组件非常相似。这是 PaneSwitchEvent

class PaneSwitchEvent extend AWTEvent
{
  public static final int COLLAPSE = 1 ;
  public static final int
  RESTORE =2 ; private int _type ;
  public PaneSwitchEvent( Object source, int t )
  {
        super( source , 0 ) ; _type = t ;
  }
  public boolean isRestore()
  {
        return _type == RESTORE ;
  }
}

面板切换事件只需要维护它表示的事件类型。

3.3. 创建侦听器接口

让我们沿着熟悉的道路继续前进,看看 PaneSwitchEventListener。这是它

interface PaneSwitchListener extends EventListener
{
  public void restore( PaneSwitchEvent e ) ;
  public void collapse( PaneSwitchEvent e ) ;
}

您的侦听器定义了面板切换事件发生时调用的两个方法。在本例中,它是 restorecollapse

3.4. 事件多路广播器

多路广播器也非常相似。就像在电子邮件示例中一样,您必须创建接受面板切换侦听器作为参数的 addremove 方法。然后创建 collapserestore 方法。请注意,您不需要为您创建的每个事件类创建一个新的多路广播器。您可以选择为您的所有组件创建一个单一的多路广播器类。我已选择将两个示例中的事件组合到一个多路广播器类中。有关详细信息,请参见列表 4。

3.4. 连接起来

最后,我们需要完成 WindowBar 组件。此组件只需将其文本从“collapse”更改为“restore”,然后在单击时再次更改回来。此外,它还会发送相应的事件。查看列表 6 以查看它是如何完成的。

3.6. 读者练习
  • 可视化窗口栏:创建 WindowBar 的子类,以图形方式渲染自身,而不是作为按钮。阅读有关鼠标侦听器和鼠标事件的信息;您将需要侦听它们。

  • 更完整的窗口栏:考虑其他操作,例如最大化、关闭等。哪些类会更改以及以何种方式更改?

示例 4. 折叠面板

折叠面板组件是一个容器,其中恰好包含一个组件。它在顶部提供一个窗口栏,包含的组件占据其余区域。单击栏时,组件折叠以仅显示窗口栏。再次单击栏时,组件将恢复。此组件不需要生成事件。但是,它确实需要侦听来自窗口栏的事件并根据这些事件采取行动。

4.1. 布局

CollapsingPane 类是 Panel 的子类。我安装 BorderLayout 作为其布局管理器。将要折叠的组件安装在中心区域,窗口栏安装在北部。

class CollapsingPane extends Panel implements
        SwitchPaneListener {
        public CollapsingPane( Component c ) {
                setLayout( new BorderLayout() ) ;
                WindowBar bar = new WindowBar() ;
                add( "North", bar ) ;
                add( "Center", c ) ;
                bar.addCollapseListener( this ) ;
        }
}
4.2. 处理事件

此类不生成事件,但它必须处理事件。它正在侦听切换面板事件。当它收到 collapse 事件时,它必须折叠,并在收到 restore 事件时恢复自身。我们已经看到了如何侦听事件并捕获它们(查看列表 7中的 collapserestore 方法)。现在,让我们看看收到事件后该怎么做。特别是,您如何折叠组件?AWT 中的每个组件都可以将其可见性设置为 true 或 false。但仅设置其可见性是不够的;您还必须重新计算布局并重新显示父组件。以下是我如何做的

private void redraw() {
        Component x = _containedComponent ;
         while( x.getParent() != null )
                {
                x = x.getParent() ;
                }
        x.validate() ;
        x.repaint() ;
}

此方法只是在组件树中向上搜索,直到找到顶层窗口(很可能是您的小程序,但它可能是自由浮动的窗口或应用程序框架)。找到顶层窗口后,我要求它验证。这将导致重新计算布局(不包含不可见的项)并重新显示。图 8 显示了折叠面板组件的示例。此示例使用电子邮件输入组件作为折叠的组件。

Building Reusable Java Widgets

图 8. 在小程序中使用的 CollapsingPane 组件

4.3. 读者练习

修改类以接受任何任意窗口栏。您可以通过添加方法来设置窗口栏或创建新的构造函数来执行此操作。请记住,为了使类能够工作,您安装的窗口栏必须是 WindowBar 的子类。此外,您可能会想到一种使用接口来消除该限制的方法。

结论

我们已经讨论了设计可重用 Java 组件的技术。生成的组件功能强大且具有足够的插拔性,足以保证所涉及的额外努力。本文是一个起点,我鼓励您探索和尝试您自己的组件的新颖和创新想法。我选择专注于功能和设计目标。我强烈建议您扩展这些组件,使其具有视觉“冲击力”。

您可以从 Linux Journal 的 FTP 站点(参见资源)或 Harris 网站 http://www.hisd.harris.com/Capabilities/java/ 下载本文中的所有示例。在 Harris 网站上,您将能够看到小程序在运行,并探索一些更高级的类似组件的迭代。

Building Reusable Java Widgets
R. J. (Bob) Celestino 拥有机械工程专业的本科学位以及电气和计算机工程专业的高级学位。他一直是 Linux 的忠实拥护者超过四年。当不重新编译他的内核或将 Java 推向极限时,他喜欢与他的妻子和三个孩子共度时光。他通过在阳光明媚的佛罗里达州 Harris Corp. 担任软件工程师来支付账单。可以通过电子邮件 celestinor@acm.org 与他联系。
加载 Disqus 评论