JVM桌面框架的状态:Swing


这篇文章重点介绍Swing API。

样例应用程序

我们需要一个简单的应用程序来进行比较。Web提供了许多应用程序构想:

  • A TODO list
  • A quiz
  • A calculator
  • A client over a web API e.g. Reddit, Twitter, etc.
  • Conway’s game of life
  • etc.

几年前,我为Champions角色扮演游戏开发了一个自定义骰子滚动应用程序。但这很复杂,需要大量的工作。

过去,当我尝试Griffon时,我使用了文件重命名器示例。简而言之,它允许选择一个文件夹并在正则表达式的帮助下对其子文件运行批处理重命名命令。线框如下所示:

20210220125907.png

用户与应用程序的交互如下:

20210220130222.png

还有其他一些规则适用:

  • 当“文件浏览器”弹出窗口打开时,将使用“文件夹”字段值进行初始化。
  • 在表格中,如果候选名称与当前名称不同,即如果重命名更改了名称,则将单元格的背景涂成黄色
  • 一个可以选择文件夹,但不能选择文件 该示例的好处有两个:业务逻辑非常有限,而GUI具有足以引起人们关注的行为。

Swing快速概述

考虑到Swing的时代,网上可以找到很多资料。不过,让我们快速了解一下Swing框架。

Bofore Swing

Swing不是第一个Java框架。该荣誉属于AWT。

AWT是第一个GUI框架,从1.0开始可用。它是特定于系统的图形对象之上的薄抽象层。因此,AWT应用程序显示并使用平台的外观。因此,AWT控件在Java中被称为重量级控件。

在1.2版本中,Java提供了Swing。尽管AWT依赖于OS图形,但是Swing会绘制每个组件本身。为此,它依赖于Java 2D图形库。Java 2D和Swing组成Java基础类。

由于它独立于操作系统,因此与AWT相比,Swing具有两个主要优点:

  1. 大量的小部件目录。AWT受所有提供JDK的操作系统上可用的小部件集的限制。没有这种限制,Swing可以提供任何小部件。

  2. 与操作系统无关的外观。OS LAF限制了AWT。由于Swing实现了小部件的绘制,因此它可以(并且确实)提供可插入的LAF。例如,Metal是独立于基本OS的小屋,它还提供了基于特定OS的多个LAF。 您可以在启动时设置LAF,并在应用程序生命周期中动态更改它。

您也可以设计自定义LAF,尽管它并不简单。

另一方面,Swing应用程序比AWT应用程序消耗更多的内存。

组件概述

对于开发人员而言,Swing类层次结构是非常标准的。父类是一个抽象 JComponent类。一Container类表示有子组件的组件。

20210220131120.png

如图所示,Swing使用了一些AWT类。

从理论上讲,这允许您混合使用两个框架的组件。但是,您通常应该避免这样做,因为重量级的AWT组件与(轻量级的)Swing组件混合使用可能会导致奇怪的行为。

大事记

Swing提供了完善的事件系统。它基于经典的Observer模式。

例如,这是的JButton类图的简化视图ActionListener:

20210220131209.png

布局

可以使用经典布局,例如BoxLayout-可以是垂直或水平的-和GridLayout。通过组合它们可以设计任何应用程序。

对于大多数应用程序,布局的嵌套将太深。嵌套布局的一种强大替代方案是GridBagLayout:,它可以将组件精确地放置在父容器上。

20210220131252.png

Swing在所有可用布局中提供了统一的API。因此,该Container.add()方法的第二个参数的类型为Object。例如,在Container具有的上GridBagLayout,第二个参数必须是一个GridBagConstraints对象。

当然,泛型将允许更严格的类型检查。但是,Swing在很久以前就被设计出来,而泛型和API从未使用过它们-而且由于Swing的地位也从未使用过。

在Java中,代码是样板代码y。由于扩展功能和命名/默认参数,Kotlin可以对其进行改进。

private fun constraints(
  gridx: Int = 0, gridy: Int = 0, gridwidth: Int = 1, gridheight: Int = 1,
  weightx: Double = 0.0, weighty: Double = 0.0, anchor: Int = CENTER,
  fill: Int = NONE, insets: Insets = Insets(0, 0, 0, 0),
  ipadx: Int = 0, ipady: Int = 0) = GridBagConstraints().apply {            
  // Apply the parameters
}

private fun JPanel.add(vararg components: Pair<JComponent,                  
                       GridBagConstraints>) {
  components.forEach {                                                      
      add(it.first, it.second)
  }
}

JPanel().add(
  JLabel("Folder:") to constraints(insets = Insets(4, 4, 4, 0)),            
  DirectoryTextField to constraints(gridx = 1, fill = HORIZONTAL, weightx = 1.0, gridwidth = 2), 
  FolderPickerButton to constraints(gridx = 3, anchor = LINE_END, insets = Insets(4, 0, 4, 0))  
)
  1. 允许定义非默认值
  2. 接受任意数量的参数
  3. 循环对以添加它们
  4. 添加具有定义约束的组件

得到教训

以下是我学到的课程,不分先后顺序:

Event Bus for the win

这不是特定于Swing的:在事件生产者和事件侦听器之间引入事件总线可以使后者与前者分离。如果您想了解有关事件总线的更多信息,请阅读有关该主题的先前文章。

在示例中,事件总线实现是Green Robot。

事件模型

要重命名,“应用”按钮需要数据:路径,正则表达式和替换项。这些数据在三个不同的文本字段中可用。

一个人如何访问按钮中的这些数据?至少有两种不同的方法可用:

  1. 在按钮中存储对字段的引用
  2. 每次字段值更改时,发送带有新值的事件,使按钮监听这些事件,并存储事件值

此外,当正则表达式或替换文本字段的值更改时,应用程序需要刷新表的右列。

我更喜欢第二种选择,将组件彼此分离。

这是流程的表示形式:

20210220131338.png

PathModel是没有GUI的单例。在事件总线的帮助下,它使我们可以在一个地方聆听事件,然后将其分发。

组件不可滚动

尽管没有人期望文本字段或按钮可滚动,但对于文本框和表而言却是另一回事。但是默认情况下,Swing组件是不可滚动的。为了使这种组件可滚动,需要将其嵌入JScrollPane组件中。

可以以一种细粒度的方式自定义滚动窗格,但是它可与一起使用JTable。例如,默认情况下,列标题始终可见。

20210220131426.png

文本字段模型和事件

摆动解耦它们显示数据组件:JTable存储其数据的TableModel,JComboBox在一个ComBoxModel,JTextField在一个Document等

关于事件,模型是要监听的对象。例如,Document在其内容更改时提供细粒度的事件:适当的更改,插入和删除。

以下类图是摘要:

20210220131510.png

在示例应用程序中,当内容更改时,不同的文本字段需要发送事件。通过利用Kotlin,可以在集中的地方处理此问题:

abstract class FiringTextField<T>(private val eventBus: EventBus,
                                  private val create: (String) -> T) : JTextField() {

  private val Document.text: String
    get() = getText(0, length)

  init {
    document.addDocumentListener(object : DocumentListener {
      override fun changedUpdate(e: DocumentEvent) = postChange(e)        
      override fun insertUpdate(e: DocumentEvent) = postChange(e)         
      override fun removeUpdate(e: DocumentEvent) = postChange(e)         
    })
  }

  private fun postChange(e: DocumentEvent) =
    eventBus.post(create(e.document.text))
}

object ReplacementTextField : FiringTextField<ReplacementUpdatedEvent>(
    EventBus.getDefault(),
    { ReplacementUpdatedEvent(it) }
)

以相同的方式处理每个更改

穿线

Swing线程模型很容易出错。最重要的规则是,不应在称为Event Dispatch Thread的主事件线程上运行任何长时间运行的任务。

事件分发线程上的任务必须快速完成;如果没有,则将备份未处理的事件,并且用户界面将变得无响应。 —事件调度线程

两种方法是可能的:

  1. 使所有对事件总线的订阅都是异步的。事件总线在专用线程而不是EDT上调用以这种方式触发的方法。在这种情况下,开发人员需要在必要时在EDT上显式运行代码。
  2. 保留默认的同步行为。在公交车上张贴事件符合“快速完成”的定义。但是随后,Runnable需要包装长期运行的任务。可以SwingUtilities.invokeLater()用来启动它。 在示例应用程序中,我赞成第二种方法,因为重命名是唯一可能长时间运行的任务。

其他外卖

无需设计小部件即可从头开始选择文件。Swing提供了JFileChooser显示此类可配置小部件的类。

jfilechooser.jpg

对象需要协作。我们已经写了有关事件总线来管理运行时事件的文章。要组装组件,还有其他方法:依赖注入是一种非常流行的方法。对于一个简单的应用程序(甚至更是一个GUI),单例已绰绰有余。例如,TableModel和JTable可以是单例:

object FileModel : AbstractTableModel() {
  // Initialization
}

object FileTable : JTable(FileModel) {
  // Initialization
}

结论

虽然很老,但Swing仍然可以完成工作。这是我们将比较其他方法的基准。

如果您有兴趣,可以自己运行该应用程序。


原文链接:http://codingdict.com