Skip to content

10. [TD]:使用 Swing接口实现[ui]层

关键词:多层架构、Spring、依赖注入、Swing组件库。

10.1. Support

在 [UI] 层中,我们希望构建一个 Swing 图形界面。NetBeans 提供了一个 [Matisse] 工具来构建这些 Swing 界面,其功能优于 Eclipse 所能提供的。Swing 界面正逐渐被 JavaFx 界面所取代。 NetBeans和Eclipse使用相同的工具来构建后者。因此,如果构建JavaFx界面,可以在分层架构中全程使用Eclipse。

NetBeans可以打开任何Maven项目。因此,我们将使用之前的Maven项目并为其添加一个Swing界面。在[2]中,我们加载(File / Open project)使用Eclipse构建的三个层级的Maven项目。然后,我们构建它们的[3]二进制文件。 选项 [Build] 和 [Clean and Build] 用于构建其所应用项目的二进制文件。这些二进制文件被放置在项目目录 [target] [4-5] 中:

选项 [Clean] 会删除该文件夹 [target]。选项 [Build] 则会重建该文件夹。 经验表明,当遇到意外问题时,首先应针对该项目执行 [Clean and Build],以确保正在使用该项目的最新版本。当存在配置文件时,这一点尤为必要——这些文件即使被修改,在执行项目时也不会触发自动重新编译。 此时需通过 [Clean and Build] 强制重新编译,以便将新版本安装到 [target] 文件夹中。

10.2. 应用程序的工作原理

让我们回到 [Elections] 应用程序的整体架构:

现在我们关注 [ui] 层的新实现。目前唯一实现的版本是一个控制台界面。现在我们将创建一个图形用户界面。

用户将通过以下界面与应用程序 [Elections] 进行交互:

图形用户界面位于 [ui] 层中。该界面负责与用户进行交互。

  • 启动时,控制台应用程序 [main] 通过 Spring 实例化应用程序的三个层。这一步操作在图形用户界面显示之前就已完成。 同样在初始化阶段,系统会向 [dao] 层请求选举相关信息(待填补席位数、选举门槛、参选名单)。 若初始化阶段失败(例如无法访问数据),控制台将显示错误信息,且不会显示图形用户界面。
  • 若数据读取成功,则显示图形界面并包含以下信息(参见上方截图):
    • (2)中待填补的席位数量
    • (3)中的选举门槛
    • 候选名单的标识符和名称在(4)中
  • 用户随后通过字段 4(ID - 名称)、5(票数)、6(添加)将票数分配给每个候选名单。

Image

  • 随后可通过链接 (10) 计算席位:

Image

  • 链接 [Enregistrer] (12) 用于将结果保存到数据源中。

10.3. [ElectionsSwing] 类是 [ui] 层的实现类

10.3.1. NetBeans 项目

:第 22.4 节说明了如何获取 NetBeans。

该应用程序的最终 NetBeans 项目将采用以下形式:[1]。请按照步骤 [2-5] 进行构建:

请确保项目已配置为通过 JDK 1.8 [1-6] 进行编译:

10.3.2. Maven 配置

新项目 [elections-swing-metier-dao-jdbc] 将基于之前的项目 [elections-console-metier-dao-jdbc]。为此,需按以下方式添加 Maven 依赖项 [1-3]:

10.3.3. 构建图形用户界面

要创建图形用户界面,我们可以按以下步骤进行:

  • [1]:向 [elections.ui.service] 包中添加一个对象
  • [2]:在类别 [Swing GUI Forms] 中选择选项 [JFrame Form]
  • [4]:为类命名
  • [5]:类的包。
  • 完成向导。
  • [6]:生成的类
  • [7]:[AbstractElectionsSwing] 类在 [Design] 模式下
  • [8]:显示窗口组件树([9])的 [Navigator] 选项卡
  • [10]:[Properties] 选项卡,用于显示在 [9] 中选中的 [jFrame] 组件的属性
  • [11]:[JFrame] 是一个组件容器。这些组件可以按照称为 layouts 的各种定位规则在容器中进行排列。 在此,我们选择 layout [Free Design] [14],它允许在容器中自由定位组件。

我们可以在名为 Palette 的工具栏中找到这些组件:

  • [1]:调色板
  • [2]:一个 JLabel 组件被放置在组件容器中
  • 右键单击它,即可访问各种属性:其名称 [4]、其文本 [3] 或其事件处理程序 [5]。 我们使用 [3] 来设置文本 [6]。
  • [1]:[JLabel] 组件的 [Properties] 选项卡可访问其属性:其水平定位 [2]、 垂直位置 [3]、文本字体 [4] 以及文本内容 [5]。

当在图形界面中放置并配置好一个组件,并保存(Ctrl-S)所做的工作后,会在类 [AbstractElectionsSwing] 中生成代码:

 
 

请勿修改这些灰色的代码,因为它们会在下次保存时被删除并重新生成。此时所做的修改将会丢失。

关于使用 NetBeans 创建图形界面的教程,请参见 URL [https://netbeans.org/kb/docs/java/quickstart-gui.html?print=yes#design](2015年11月)。

现在我们构建以下界面:

该界面的组件如下:

编号
类型
名称
作用
1
JMenuBar
jMenuBar1
一个菜单
2
JLabel
jLabelSAP
待填补的席位数
3
JLabel
jLabelSE
选举门槛
4
JComboBox
jComboBoxNomsListes
参选名单
5
JTextField
jTextFieldVoixListe
某候选名单的得票数
6
JLabel
jLabelAjouter
向 (8) 添加一个列表
7,8
(JScrollPane, JList)
jListNomsVoix
列表的名称和语音
9
JLabel
jLabelSupprimer
从 (8) 中删除在 (8) 中选定的列表
10
JLabel
jLabelCalculer
用于计算选举结果
11
JLabel
jLabelEffacer
用于清除选举结果
12
JLabel
jLabelEnregistrer
用于保存选举结果
13,14
(JScrollPane, JList)
jListResultats
用于显示选举结果
15,16
(JScrollPane,
JTextPane)
jTextPaneMessages
用于显示跟踪信息

注释 (JScrollPane, JList) [13-14] 用于指示:当将组件 [JList] 拖放到窗口中时,它会自动插入到组件 [JScrollPane] 中,该组件支持列表滚动。 [JScrollPane]组件用于显示列表中的所有项目,尽管在特定时刻仅能显示其中的一部分。[JTextPane]和[15-16]组件也是如此。

菜单可按以下方式创建:

  • [1, 2]:在窗口中放置一个 [Menu Bar] 组件
  • [3]:默认生成的菜单,如[Navigator]选项卡中所示
  • [4,5,6]:右键单击菜单选项,可:
    • 修改其文本 [4]、名称 [5]
    • 管理其事件 [6]
  • [7]:所需的菜单

所需的菜单如下:

一级
二级
选举
 
 
Quitter
候选人名单
 
 
Ajouter
 
Supprimer
结果
 
 
Calculer
 
Effacer
 
Enregistrer
关于
 

您可以随时测试图形界面:

 

在构建界面时,需为某些标签和菜单关联事件处理程序 [Ajouter, Effacer, ...]。具体操作如下:

  • [1]:右键单击需要管理事件的组件
  • [2]:选择选项 [Events]
  • [3]:选择事件类别
  • [4]:选择要管理的事件

此操作生成的 Java 代码如下:


    jLabelCalculer.addMouseListener(new java.awt.event.MouseAdapter() {
      public void mouseClicked(java.awt.event.MouseEvent evt) {
        jLabelCalculerMouseClicked(evt);
      }
    });
...

  private void jLabelCalculerMouseClicked(java.awt.event.MouseEvent evt) {
    // TODO 请在此处添加您的处理代码:
}
  • 第 1-5 行:向组件 jLabelCalculer 添加了一个事件处理程序。方法 addMouseListener 期望的参数是一个实现以下接口 MouseListener 的类:
 

接口 MouseListener 由多个类实现,其中包括类 MouseAdapter。该类实现了接口 MouseListener 的五个方法,但这些方法不执行任何操作。 因此,必须继承该类以实现所需的方法。上述代码中已实现此操作,具体如下:


    jLabelCalculer.addMouseListener(new java.awt.event.MouseAdapter() {
      public void mouseClicked(java.awt.event.MouseEvent evt) {
        jLabelCalculerMouseClicked(evt);
      }
});

上述代码使用了课程 [ref1] 第 2.5 节中介绍的匿名类技术。

第 1 行,方法 addMouseListener 的参数是一个匿名类,该类是在运行时动态定义的。 这是一个从类 MouseAdapter(第 1 行)派生而来的类的实例,其中重写了方法 mouseClicked(第 2-4 行),使其能够执行特定操作。

名为“第3行”的方法jLabelCalculerMouseClicked定义如下:


  private void jLabelCalculerMouseClicked(java.awt.event.MouseEvent evt) {
    // TODO 请在此处添加您的处理代码:
}

开发人员通过在此方法中编写代码来处理“MouseClicked”事件。

NetBeans 生成的所有事件处理程序均遵循此模式。开发人员可以忽略 NetBeans 生成的代码行,直接将方法与组件事件关联。只需在上述第 2 行中编写自己的代码即可。以下是一个示例:


  private void jLabelCalculerMouseClicked(java.awt.event.MouseEvent evt) {
    System.out.println("Mouse Clicked");
}

如果运行图形界面并点击链接 [Calculer],控制台将显示一条消息:

  • [1]:双击标签 [Calculer]
  • [2]:该事件的处理程序已执行,并在 NetBeans 控制台中生成 mouseClicked 消息。

[jComboBoxNomsListes, jListNomsVoix, jListResultats] 组件的声明如下:


protected javax.swing.JComboBox jComboBoxNomsListes;
protected javax.swing.JList jListNomsVoix;
protected javax.swing.JList jListResultats;

这些组件是列表,通常由类型 T 进行配置:即组件所显示模型中元素的类型。该类型 T 可以是任意类型。 列表组件中显示的值类型为 [String]。默认情况下,将使用方法 [T.toString()] 进行显示。为了更好地控制显示内容,此处的类型 T 将设为 String 类型。因此,我们列表的正确声明如下:


protected javax.swing.JComboBox<String> jComboBoxNomsListes;
protected javax.swing.JList<String> jListNomsVoix;
protected javax.swing.JList<String> jListResultats;

通过修改组件的某个属性,即可获得此结果:

10.3.4. 代码分离

让我们回到应用程序的结构:

类 [AbstractElectionsSwing] 必须实现上方的 [ui] 层。目前由 NetBeans 生成的代码仅包含窗口管理代码以及目前尚无实际作用的事件处理程序。 上文可见,类 [AbstractElectionsSwing] 需负责与 [métier] 层之间的交互。该交互将在事件处理程序中实现。为清晰起见,我们决定将其拆分为两个类:

  • [AbstractElectionsSwing],该类除少量细节外将保留NetBeans生成的原始结构。该类本身不会处理任何事件。其事件处理程序将保持空状态并声明为抽象类,具体实现将由继承自[AbstractElectionsSwing]的子类完成。
  • [ElectionsSwing],该类继承自 [AbstractElectionsSwing],并将实现所有事件处理程序。

这种分离方式并不罕见。例如,在网页 ASP.NET(非 MVC 版本)中就能看到。NetBeans 项目的发展如下:

 

而 [AbstractElectionsSwing] 类的代码演变如下:


public abstract class AbstractElectionsSwing {
....
    private void jMenuItemCalculerActionPerformed(java.awt.event.ActionEvent evt) {
        doCalculer();
    }

...

    private void jLabelCalculerMouseClicked(java.awt.event.MouseEvent evt) {
        if (jLabelCalculer.isEnabled()) {
            doCalculer();
        }
    }

....
    // 事件处理程序
    abstract protected void doSupprimer();

    abstract protected void doCalculer();

    abstract protected void doQuitter();

    abstract protected void doEffacer();

    abstract protected void doEnregistrer();

    abstract protected void doAjouter();

    abstract protected void doInformer();

    abstract protected void doMajLabelAjouter();

    abstract protected void doMajLabelSupprimer();
...
}
  • 第 1 行:声明该类为抽象类
  • 第 3-5 行:处理对菜单选项 [jMenuItemCalculer] 的点击。 可以看到,事件处理被委托给第19行的doCalculer方法。该方法未被实现,且被声明为抽象方法。它将由派生类[ElectionsSwing]来实现;
  • 第 9-13 行:标签 [jLabelCalculer] 上的事件处理程序 clic。 无论组件 [jLabel] 处于活动状态(enabled=true)还是非活动状态(enabled=false),点击操作都会触发事件。此处需确保其处于活动状态以处理该事件;
  • 第 15 行及之后:将事件处理委托给抽象方法的这种技术被应用于所有事件处理程序。

从 [AbstractElectionsSwing] 派生的类 [ElectionsSwing] 实现了 [AbstractElectionsSwing] 未实现的所有事件处理程序:


package elections.ui.service;;
...
public class ElectionsSwing extends AbstractElectionsSwing {

    // 事件处理程序

    @Override
    protected void doInformer() {
...
    }

    @Override
    protected void doAjouter() {
    ...
    }

    @Override
    protected void doCalculer() {
    ...
    }

    @Override
    protected void doEffacer() {
    ...
    }

    @Override
    protected void doEnregistrer() {
...
    }

    @Override
    protected void doQuitter() {
        System.exit(0);
    }

    @Override
    protected void doSupprimer() {
...
    }

    @Override
    protected void doMajLabelAjouter() {
...
    }

    @Override
    protected void doMajLabelSupprimer() {
...
    }

}
  • 第 3 行:[ElectionsSwing] 派生自 [AbstractElectionsSwing]
  • 第 7-50 行:图形窗口的事件处理程序

派生类 [ElectionsSwing] 的方法将操作父类 [AbstractElectionsSwing] 的组件。 目前这些组件的作用域为 private,禁止子类 [ElectionsSwing] 访问它们:


private JMenuItem jMenuItemAPropos = null;

private JLabel jLabelAjouter = null;

为解决此问题,我们将确保图形用户界面组件的作用域为 [protected]:

  • 将 [protected] 属性设置为 [3];

10.3.5. [IElectionsUI] 接口的实现

让我们回到应用程序的结构:

如上所述,[ui]层必须向[main]对象提供[IElectionsUI]接口:


package elections.ui.service;

public interface IElectionsUI {
    /**
     * lance le dialogue avec l'utilisateur
     */
    public void run();
}

该接口在项目 [elections-console-metier-dao-jdbc] 中定义,并在第 9.4 节中进行了描述。由于该项目是项目 [swing] 的依赖项,因此该接口已被识别。

由于类 [AbstractElectionsSwing] 已变为抽象类,因此 Spring 无法再对其进行实例化。现在应实例化类 [ElectionsSwing]。 类 [ElectionsSwing] 必须实现接口 [IElectionsUI]。因此,其代码如下所示:


public class ElectionsSwing extends AbstractElectionsSwing implements IElectionsUI {

    // [ElectionsUI] 接口的 run 方法
    public void run() {
...
    }

  • 第 1 行:类 [ElectionsSwing] 实现了接口 [IElectionsUI]
  • 第 4-6 行:该接口的 [run] 方法

方法 run 应该做什么?显示图形窗口。如何实现这一点?我们可以参考 NetBeans 在类 [AbstractElectionsSwing] 中生成的方法 [main],该方法实现了所需的功能:


public static void main(String args[]) {
    /* 设置 Nimbus 外观和感觉 */
    //<editor-fold defaultstate="collapsed" desc=" 外观和感觉设置代码(可选) ">
    /* 如果 Nimbus(在 Java SE 6 中引入)不可用,则保持默认外观和感觉。
         * For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html 
     */
    try {
      for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {
        if ("Nimbus".equals(info.getName())) {
          javax.swing.UIManager.setLookAndFeel(info.getClassName());
          break;
        }
      }
    } catch (ClassNotFoundException ex) {
      java.util.logging.Logger.getLogger(NewJFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
    } catch (InstantiationException ex) {
      java.util.logging.Logger.getLogger(NewJFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
    } catch (IllegalAccessException ex) {
      java.util.logging.Logger.getLogger(NewJFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
    } catch (javax.swing.UnsupportedLookAndFeelException ex) {
      java.util.logging.Logger.getLogger(NewJFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
    }
    //</editor-fold>

    /* 创建并显示表单 */
    java.awt.EventQueue.invokeLater(new Runnable() {
      public void run() {
        new NewJFrame().setVisible(true);
      }
    });
  }

第 28 行使用的 [AbstractElectionsSwing] 构造函数如下:


  public AbstractElectionsSwing() {
    initComponents();
}
  • 第 2 行:方法 [initComponents] 是由图形界面生成器生成的私有方法。其代码无法修改。

因此,类 [ElectionsSwing] 中的方法 [run] 可能如下所示:


  @Override
  public void run() {
    // 显示图形界面
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        init();
        setVisible(true);
      }
    });
}
  • 第 6 行:通过方法 [init] 初始化图形用户界面。此处希望调用父类的 [initComponents] 方法,但该方法为私有。 因此,在父类 [AbstractElectionsSwing] 中添加以下方法 [init]:

  protected void init(){
    initComponents();
}
  • (续)
    • 由于位于类 [AbstractElectionsSwing] 中,方法 [init] 可以访问同一类中的私有方法 [initComponents];
    • 由于它具有属性 [protected],因此在子类 [ElectionsSwing] 中可见;
  • 第 7 行:图形界面被设为可见;

:当在类 [ElectionsSwing] 中编写了方法 [run] 后,抽象类 [AbstractElectionsSwing] 中的方法 [main] 即可被删除。

10.3.6. 可执行类

让我们回到应用程序的结构:

我们希望 Spring 能像当初由 console 应用程序实现该层时那样,对 [ui] 层进行实例化。 为此,实现类 [ElectionsSwing] 必须引用 [métier] 层:


@Component
public class ElectionsSwing extends AbstractElectionsSwing implements IElectionsUI{

  // 关于 [métier] 层的参考
  @Autowired
  private IElectionsMetier metier;
...
  • 第 1 行:类 [ElectionsSwing] 是 Spring 组件;
  • 第 5-6 行:Spring 注入对 [métier] 层的引用;

图形界面通过执行以下 [BootElectionsSwing] 类启动:

  

package elections.ui.boot;

import elections.ui.service.IElectionsUI;

public class BootElectionsSwing extends AbstractBootElections {
    public static void main(String[] arguments) {
        new BootElectionsSwing().run();
    }

    @Override
    protected IElectionsUI getUI() {
        return ctx.getBean("electionsSwing", IElectionsUI.class);
    }
}

我们在第 9.5 节讲解 [AbstractBootElections] 和 [BootElectionsConsole] 类代码时,已对类似的代码进行过说明。 第12行,我们获取名为[electionsSwing]的Bean,该名称对应类[ElectionsSwing]的Spring标准名称。

10.3.7. 图形界面的初始化

当图形界面显示时,其部分组件已初始化:

Image

上图显示:

  • 下拉列表已填充了候选名单的名称;
  • 待选席位数和选举门槛已显示;
  • 部分链接已被禁用;
  • 窗口底部显示了一条成功提示;

这些初始化操作将在何时进行?它们只能在类 [ElectionsSwing] 的字段 [electionsMetier] 初始化完成后进行。因为候选名单的名称将从 [metier] 层获取。 该字段的初始化将由 Spring 按以下顺序执行:

  • 调用类 [ElectionsSwing] 的无参构造函数;
  • 注入依赖项,此处为 [métier] 层的引用;
  • 执行类 [ElectionsSwing] 的方法 [run]:

    @Override
    public void run() {
        // 显示图形界面
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                init();
                setVisible(true);
            }
        });
}
  • 第12行,我们提到将调用父类的[init]方法,该方法将绘制图形用户界面的组件。 我们将在此类 [ElectionsSwing] 中重写该方法。正是在此方法中,我们将使用数据初始化窗口组件(下拉列表、标签):

局部方法 [init] 的骨架可能如下所示:


public class ElectionsSwing extends AbstractElectionsSwing implements IElectionsUI {

    ...
  // 关于 [métier] 层的参考
  @Autowired
  private IElectionsMetier metier;

    // 初始化
    public void init() {
      // 由父类生成组件
    super.init();
    
    // 向 [metier] 层请求列表
        ...
        // 将列表名称与下拉列表 jComboBoxNomsListes 关联
        ...
        // 以及选举参数
        ...
        // 初始化与这两项信息相关的标签
        ...
        // 成功消息
        ...
        // 初始化某些表单组件的状态
...
}

请注意第 11 行,这里调用了父类的 [init] 方法。

10.3.8. [Utilitaires] 类

一些静态实用方法被整合到了 [Utilitaires] 类中:

 

[Utilitaires] 类如下:


package istia.st.elections.ui;

import javax.swing.JLabel;
import javax.swing.JMenuItem;

//辅助类
class Utilitaires {
    // 管理标签数组的状态
    public static void setEnabled(JLabel[] labels, boolean value) {
        for (int i = 0; i < labels.length; i++) {
            labels[i].setEnabled(value);
        }
    }

    // 管理菜单选项数组的状态
    public static void setEnabled(JMenuItem[] menuItems, boolean value) {
        ...
    }

}
  • 第 9 行:方法 setEnabled 设置数组中定义的 JLabel 组件的状态。 JLabel 组件的 setEnabled 方法可用于启用或禁用 JLabel

待完成任务: 参照第 9setEnabled 方法的示例,编写第 16 行 setEnabled 方法,该方法对 JMenuItem 组件执行相同操作。


10.3.9. [ElectionsSwing]类的代码

回顾一下类 [ElectionsSwing] 的总体结构:


package istia.st.elections.ui;

...
public class ElectionsSwing extends AbstractElectionsSwing implements IElectionsUI {

  // 关于 [métier] 层的参考
  @Autowired
  private IElectionsMetier metier;


    // 初始化
    public void init() {
    ...
    }

    // 事件处理程序

    @Override
    protected void doInformer() {
...
    }

    @Override
    protected void doAjouter() {
...
    }

    @Override
    protected void doCalculer() {
    ...
    }

    @Override
    protected void doEffacer() {
...
    }

    @Override
    protected void doEnregistrer() {
...
    }

    @Override
    protected void doQuitter() {
        System.exit(0);
    }

    @Override
    protected void doSupprimer() {
...
    }

    @Override
    protected void doMajLabelAjouter() {
    ...
    }

    @Override
    protected void doMajLabelSupprimer() {
...
    }

}

我们将依次研究该类的各个方法。

10.3.9.1. [init]方法

让我们回到图形界面:

方法 [init] 的目标是:

  • 在组合字段 [4] 中填写列表的标识符和名称,格式为 [id - nom]
  • 在 [15] 中显示一条成功消息
  • 初始化标签 [2] 和 [3]
  • 禁用某些链接

方法 [init] 的框架可能如下:


@Override
    protected void init() {
        // 由父类生成组件
        super.init();
        // 本地初始化
        modèleNomsVoix = new DefaultListModel<>();
        jListNomsVoix.setModel(modèleNomsVoix);
        modèleRésultats = new DefaultListModel<>();
        jListResultats.setModel(modèleRésultats);
        String info;
        try {
            // 向 [métier] 层请求列表
            listes = ...
            // 将列表名称与下拉列表关联 jComboBoxNomsListes
            ...
            // 以及选举参数
            int nbSiegesAPourvoir = ...
            double seuilElectoral = ...
            // 初始化与这两项信息相关的标签
            ...
            // 成功消息
            info = "Source de données lue avec succès";
        } catch (ElectionsException ex1) {
            // 记录错误
            info = getInfoForException("Les erreurs suivantes se sont produites :", ex1);
        } catch (RuntimeException ex2) {
            // 记录错误
            info = getInfoForException("Les erreurs suivantes se sont produites :", ex2);
        }
        // 显示信息
        jTextPaneMessages.setText(info);
        jTextPaneMessages.setCaretPosition(0);
        // 表单状态
        Utilitaires.setEnabled(new JLabel[] { jLabelAjouter, jLabelCalculer, jLabelEnregistrer, jLabelSupprimer }, false);
        Utilitaires.setEnabled(
                new JMenuItem[] { jMenuItemAjouter, jMenuItemCalculer, jMenuItemEnregistrer, jMenuItemSupprimer }, false);
        // 居中显示窗口
        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        Dimension frameSize = getSize();
        if (frameSize.height > screenSize.height) {
            frameSize.height = screenSize.height;
        }
        if (frameSize.width > screenSize.width) {
            frameSize.width = screenSize.width;
        }
        setLocation((screenSize.width - frameSize.width) / 2, (screenSize.height - frameSize.height) / 2);
    }

    private String getInfoForException(String message, ElectionsException ex) {
        // 显示消息
        StringBuffer info = new StringBuffer(String.format("%s -------------\n", message));
        info.append(String.format("Code erreur : %d\n", ex.getCode()));
        // 显示错误
        for (String erreur : ex.getErreurs()) {
            info.append(String.format("-- %s\n", erreur));
        }
        return info.toString();
    }

    private String getInfoForException(String message, RuntimeException ex) {
        // 显示消息
        StringBuffer info = new StringBuffer(String.format("%s -------------\n", message));
        // 显示异常堆栈
        Throwable cause = ex;
        while (cause != null) {
            info.append(String.format("-- %s\n", cause.getMessage()));
            cause = cause.getCause();
        }
        return info.toString();
    }

待完成任务:补充方法 [init] 的代码。


课程阅读内容:组件 JTextFieldJLabel

需知

JList 组件用于显示模型中的数据。默认情况下,该模型的类型为 DefaultListModel(第 2 行和第 3 行)。 DefaultListModel 对象的行为与 ArrayList 类型有些相似:

  • 要在模板中添加一个 o 对象:

[DefaultListModel].addElement(Object o);

在此应用中,对象 o 始终属于类型 String

  • 从模板中删除第 i 个元素:

[DefaultListModel].remove(int i);
  • 获取模板中的第 i 个元素:

[DefaultListModel].elementAt(int i);

要向组合 [jComboBoxNomsListes] 添加一个元素,将使用方法 [addItem]:


jComboBoxNomsListes.addItem(chaîne de caractères)

组件 JTextPane 具有方法 getText() 和 setText(),用于读取/写入显示的文本。

10.3.9.2. 管理链接状态 [Ajouter]

链接 [Ajouter] [6] 仅在“选项”字段 [5] 不为空时才处于活动状态。 在类 [AbstractElectionsSwing] 中,跟踪字段 [5] 中光标移动的处理程序如下:


  private void jTextFieldVoixListeCaretUpdate(javax.swing.event.CaretEvent evt) {
    doMajLabelAjouter()
}

第 2 行调用了类 [ElectionsSwing] 中的方法 [doMajLabelAjouter]。


    protected void doMajLabelAjouter() {
        // 设置标签状态 [jLabelAjouter]
        ...
        // 设置菜单状态 [jMenuItemAjouter]
        ...
}

待完成任务:补全方法 [doMajLabelAjouter] 的代码。


10.3.9.3. 将选票分配给各候选名单

对于 (4) 中的每个候选列表,操作步骤如下:

  • 从(4)中选择一个名单
  • 在(5)中输入票数
  • 点击链接 [Ajouter] 进行确认

输入错误将按如下示例所示进行提示:

Image

如果票数正确,该列表将被添加到组件 (8) 中,票数将被清除,且链接 [Ajouter] 将失效:

在类 [AbstractElectionsSwing] 中,处理链接 [Ajouter] 点击事件的处理程序如下:


    private void jLabelAjouterMouseClicked(java.awt.event.MouseEvent evt) {
        if (jLabelAjouter.isEnabled()) {
            doAjouter();
        }
}

第 3 行调用了类 [ElectionsSwing] 中的方法 [doAjouter]:


  // 列表模板 JList
  private DefaultListModel<String> modèleNomsVoix = null;
  private DefaultListModel<String> modèleRésultats = null;

  // 参赛列表
  private ListeElectorale[] listes;

  // 用户输入的名单
  private final List<ListeElectorale> listesSaisies = new ArrayList<>();
  private ListeElectorale[] tListesSaisies;
...
  @Override
  protected void doAjouter() {
    // 票数正确吗?
    ...
    // 如有错误,则发出提示
    if (erreur) {
      JOptionPane.showMessageDialog(null, "Nombre de voix incorrect", "Elections : erreur",
              JOptionPane.INFORMATION_MESSAGE);
      jTextFieldVoixListe.requestFocus();
      // 返回图形界面
      return;
    }
    // 无错误 - 保存列表
    listesSaisies.add(...);
    modèleNomsVoix.addElement(...);
    // 清空票数
    jTextFieldVoixListe.setText("");
    // 表单状态(菜单、标签)
...
}
  • 第25行:每当用户向列表中添加选项并确认选择时,该列表会被放入第9行的[listesSaisies]字段中。该字段将记录包含[id, version, nom, voix]信息的列表。 前三个信息来自最初存储在第6行数组中的列表。下拉列表的[getSelectedIndex]方法可用于获取所选列表的索引;

待完成任务:完善方法 [doAjouter] 的代码。


10.3.9.4. 管理链接状态 [Supprimer]

链接 [Supprimer] [9] 仅在 [8] 中选中某个元素时才处于活动状态。

在类 [AbstractElectionsSwing] 中,响应列表 [8] 中元素点击的操作处理程序如下:


  private void jListNomsVoixValueChanged(javax.swing.event.ListSelectionEvent evt) {
    doMajLabelSupprimer();
}

第 2 行调用了类 [ElectionsSwing] 中的方法 [doMajLabelSupprimer]。


    @Override
    protected void doMajLabelSupprimer() {
            // 启用标签 [jLabelSupprimer] 及其对应的菜单选项
            ...
}

待完成任务:补全方法 [doMajLabelSupprimer] 的代码。


10.3.9.5. 删除候选列表

链接 [Supprimer] [9] 用于删除在 (8) 中选定的 (名称, 选票) 对。 删除操作完成后,链接 [Supprimer] 将失效。只有在 (8) 中选定新的列表后,该链接才会重新激活。

在类 [AbstractElectionsSwing] 中,响应点击链接 [Supprimer] 的处理程序如下:


  private void jLabelSupprimerMouseClicked(java.awt.event.MouseEvent evt) {
    if(jLabelSupprimer.isEnabled()){
      doSupprimer();
    }
}

第3行调用了类[ElectionsSwing]中的方法[doSupprimer]。


@Override
    protected void doSupprimer() {
        // 删除所选列表、模板 modèleNomsVoix 以及已输入的列表
        ...
        // 更新表单中标签和菜单选项的状态
        Utilitaires.setEnabled(...);
    ...
}

待完成任务:完善方法 [doSupprimer] 的代码。


10.3.9.6. 管理链接状态 [Calculer]

链接 [Calculer] [10] 仅在 [8] 中至少存在一个元素时才处于活动状态。


待完成工作:在之前编写的 [doAjouter] 和 [doSupprimer] 方法中,添加管理此链接所需的代码。相应的菜单选项也将一并处理。


需知DefaultListModel 类型元素的子元素数量可通过 size() 方法获取。

10.3.9.7. 计算席位

通过链接 [Calculer] [10] 可启动席位计算,并在 (14) 中显示结果。 若计算失败(所有候选名单均被淘汰),[15]中将显示错误信息。无论结果如何,计算完成后,链接[Calculer] [10]将被禁用。

在类 [AbstractElectionsSwing] 中,响应 [Calculer] 链接点击的操作管理器如下:


  private void jLabelCalculerMouseClicked(java.awt.event.MouseEvent evt) {
    if(jLabelCalculer.isEnabled()){
      doCalculer();
    }
}

第 3 行调用了 [ElectionsSwing] 类的 [doCalculer] 方法。


  // 用户输入的列表
  private final List<ListeElectorale> listesSaisies = new ArrayList<>();
  private ListeElectorale[] tListesSaisies;

...
  @Override
  protected void doCalculer() {
    tListesSaisies = listesSaisies.toArray(new ListeElectorale[0]);
    // 计算席位
    try {
      ...
    } catch (ElectionsException ex) {
      // 显示异常
      ...
      return;
    }
    // 显示结果
    ...
    // 更新表单状态
    Utilitaires.setEnabled(...);
}

待完成任务:补全方法 [doCalculer] 的代码。


10.3.9.8. 将结果保存到数据源

链接 [Enregistrer] (12) 用于将席位计算结果保存到数据源中。 成功保存后,链接 [Enregistrer] 将被禁用。若保存失败,[15] 中将显示一条错误消息。无论结果如何,链接 [Enregistrer] 随后都会被禁用。

在类 [AbstractElectionsSwing] 中,处理对标签 [Enregistrer] 点击的操作管理器如下:


  private void jLabelEnregistrerMouseClicked(java.awt.event.MouseEvent evt) {
    if(jLabelEnregistrer.isEnabled()){
      doEnregistrer();
    }
}

第 3 行调用了 [ElectionsSwing] 类的 [doEnregistrer] 方法:


@Override
    protected void doEnregistrer() {
        // 向业务层请求保存
        try {
            ...
        } catch (ElectionsException ex) {
            // 显示异常
            ...
            // 返回图形界面
            return;
        }
        // 更新表单
        Utilitaires.setEnabled(...);
...
}

待完成任务:补全方法 [doEnregistrer] 的代码。


10.3.9.9. 清除结果

链接 [Effacer] (11) 可清除 (14) 中显示的结果。

在类 [AbstractElectionsSwing] 中,处理标签 [Effacer] 点击事件的处理程序如下:


  private void jLabelEffacerMouseClicked(java.awt.event.MouseEvent evt) {
    if(jLabelEffacer.isEnabled()){
      doEffacer();
    }
}

第 3 行调用了 [ElectionsSwing] 类的 [doEffacer] 方法:


    @Override
    protected void doEffacer() {
        // 清空结果列表
        ....
        // 刷新表单
        Utilitaires.setEnabled(...);
}

待完成任务:补全方法 [doEffacer] 的代码。


需知:类 DefaultListModel 有一个方法 clear(),该方法会删除其所有元素。

10.3.10. 改进

上述图形界面可通过多种方式进行改进:用户可能因疏忽而未输入下拉列表中所有列表的选项,此外,用户也可能因操作失误而多次输入同一列表的选项。


待完成任务:改进算法,确保上述两种情况均不会发生。一个简单的解决方案是维护一个已输入列表的字典,其键为下拉列表中的项目。同时,应确保仅当所有列表均已输入完毕时,链接 [Calculer] 才处于激活状态。


请参阅课程 [ref1]:第 3.8 节中的类 HashTable