Skip to content

10. [作业]:使用 Swing 界面实现 [ui] 层 -

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

10.1. 支持

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

NetBeans 可以打开任何 Maven 项目。因此,我们将使用之前的 Maven 项目,并向其中添加一个 Swing 界面。 在[2]中,我们通过“文件/打开项目”加载了使用Eclipse构建的三个层的Maven项目。随后,我们构建了这些项目的二进制文件[3]。“构建”和“清理并构建”选项会为所选项目生成二进制文件。这些二进制文件被放置在项目的[target]文件夹中[4-5]:

[清理] 选项会删除此 [目标] 文件夹。[构建] 选项会重新构建它。经验表明,当出现意外问题时,首先应执行项目的 [清理并构建] 操作,以确保您正在使用最新版本。当您拥有配置文件时,这一点尤为必要——因为这些文件即使被修改,在运行项目时也不会触发自动重新编译。 此时,您必须通过执行 [清理并构建] 来强制重新编译,以便将新版本安装到 [目标] 文件夹中。

10.2. 应用程序的工作原理

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

我们目前正专注于[ui]层的新实现。当前唯一存在的实现是一个控制台界面。我们现在正在创建一个图形用户界面。

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

图形用户界面位于[ui]层。正是这一层与用户进行交互。

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

Image

  • 随后,您可以使用链接 (10) 计算席位:

Image

  • 点击 [保存] 链接 (12) 可将结果保存至数据源。

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

10.3.1. NetBeans 项目

注意第 22.4 节介绍了如何获取 NetBeans。

该应用程序的最终 NetBeans 项目将如下所示 [1]。请按照步骤 [2–5] 进行构建:

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

10.3.2. Maven 配置

新项目 [elections-swing-business-dao-jdbc] 将基于之前的项目 [elections-console-business-dao-jdbc] 构建。为此,请按以下步骤添加 Maven 依赖项 [1-3]:

10.3.3. 构建图形用户界面

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

  • [1]: 将一个对象添加到 [elections.ui.service] 包中
  • [2]: 在 [Swing GUI 表单] 类别中选择 [JFrame 表单] 选项
  • [4]:为类命名
  • [5]: 类的包名。
  • 完成向导。
  • [6]: 生成的类
  • [7]: [AbstractElectionsSwing] 类在 [设计] 模式下
  • [8]:显示窗口组件树 [9] 的 [Navigator] 选项卡
  • [10]:[属性] 选项卡,显示在 [9] 中选中的 [JFrame] 组件的属性
  • [11]:[JFrame] 是一个组件容器。组件可以在容器内根据各种称为布局的定位规则进行排列。在此,我们选择 [自由布局] [14],该布局允许组件在容器内自由定位。

我们可以在名为“调色板”的工具栏中找到这些组件:

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

当在图形界面中拖放并配置组件,且您保存(Ctrl-S)工作时,[AbstractElectionsSwing] 类中会生成代码:

 
 

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

有关使用 NetBeans 创建图形用户界面的教程,请访问 [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
jComboBoxListNames
竞争名单的名称列表
5
JTextField
jTextFieldVotesList
列表的票数
6
JLabel
jLabelAdd
用于向 (8) 添加列表
7,8
(JScrollPane, JList)
jListNamesVoices
列表的名称和语音
9
JLabel
jLabelDelete
从 (8) 中移除在 (8) 中选中的列表
10
JLabel
jLabelCalculate
用于计算选举结果
11
JLabel
jLabelClear
用于清除选举结果
12
JLabel
jLabelSave
用于保存选举结果
13,14
(JScrollPane, JList)
jListResults
用于显示选举结果
15,16
(JScrollPane,
JTextPane)
jTextPaneMessages
用于显示后续消息

注解 (JScrollPane, JList) [13-14] 的作用是表明:当 [JList] 组件被拖放到窗口中时,它会自动插入到 [JScrollPane] 组件中,从而允许用户滚动浏览列表。 正是 [JScrollPane] 组件使您能够查看列表中的所有项目,尽管在任何给定时刻仅能看到其中有限数量的项目。这同样适用于 [JTextPane] 组件 [15-16]。

菜单的创建方法如下:

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

所需的菜单如下:

一级
二级
选举
 
 
退出
列表
 
 
添加
 
删除
结果
 
 
计算
 
清除
 
保存
关于
 

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

 

在构建界面时,您必须为某些标签和菜单[添加、删除、...]关联事件处理程序。具体操作如下:

  • [1]:右键单击您想要管理其事件的组件
  • [2]:选择 [事件] 选项
  • [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 add your handling code here:
}
  • 第 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 add your handling code here:
}

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

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


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

若运行该图形界面并点击 [计算] 按钮,控制台将显示一条消息:

  • [1]: 双击 [计算] 标签
  • [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] 类需要处理与 [business] 层的交互。这种交互将在事件处理程序中进行。为了使代码结构更加清晰,我们决定将其拆分为两个类:

  • [AbstractElectionsSwing],该类将保留 NetBeans 生成的原始结构,仅进行少量微调。该类本身不会处理任何事件。其事件处理程序将保持为空并声明为抽象方法,这些方法将由一个从 [AbstractElectionsSwing] 派生的类来实现。
  • [ElectionsSwing],这是一个从 [AbstractElectionsSwing] 派生的类,将实现所有事件处理程序。

这种分离方式并不罕见。例如,在 ASP.NET Web 页面(非 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();
        }
    }
 
....
    // event managers
    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] 的点击事件。无论 [jLabel] 组件处于活动状态(enabled=true)还是非活动状态(enabled=false),点击操作都会触发事件。此处我们确保其处于活动状态以处理该事件;
  • 第 15 行及之后:将事件处理委托给抽象方法的这种技术被应用于所有事件处理程序。

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


package elections.ui.service;;
...
public class ElectionsSwing extends AbstractElectionsSwing {
 
    // event managers
 
    @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] 的组件。目前,这些组件的访问范围为私有,因此子类 [ElectionsSwing] 无法访问它们:


private JMenuItem jMenuItemAPropos = null;
 
private JLabel jLabelAjouter = null;

为解决此问题,我们将确保 GUI 组件的作用域为 [protected]:

  • 在 [3] 中设置 [protected] 属性;

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 {
 
    // interface [ElectionsUI] run method
    public void run() {
...
    }
 
  • 第 1 行:[ElectionsSwing] 类实现了 [IElectionsUI] 接口
  • 第 4–6 行:该接口的 [run] 方法

run 方法应该做什么?显示 GUI 窗口。我们该如何实现?我们可以参考 [AbstractElectionsSwing] 类中由 NetBeans 生成的 [main] 方法,它正好实现了我们想要的功能:


public static void main(String args[]) {
    /* Set the Nimbus look and feel */
    //<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) ">
    /* If Nimbus (introduced in Java SE 6) is not available, stay with the default look and feel.
         * 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>
 
    /* Create and display the form */
    java.awt.EventQueue.invokeLater(new Runnable() {
      public void run() {
        new NewJFrame().setVisible(true);
      }
    });
  }

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


  public AbstractElectionsSwing() {
    initComponents();
}
  • 第 2 行:[initComponents] 方法是由 GUI 生成器生成的私有方法。其代码不可更改。

[ElectionsSwing] 类的 [run] 方法可以如下所示:


  @Override
  public void run() {
    // on affiche l'interface graphique
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        init();
        setVisible(true);
      }
    });
}
  • 第 6 行:使用 [init] 方法初始化 GUI。这里,我们希望调用父类 [AbstractElectionsSwing] 的 [initComponents] 方法,但该方法是私有的。因此,我们在父类 [AbstractElectionsSwing] 中添加了以下 [init] 方法:

  protected void init(){
    initComponents();
}
  • (待续)
    • 由于位于 [AbstractElectionsSwing] 类中,[init] 方法可以访问同一类的私有 [initComponents] 方法;
    • 由于它带有 [protected] 修饰符,因此可在子类 [ElectionsSwing] 中访问;
  • 第 7 行:将 GUI 显示出来;

注意:一旦在 [ElectionsSwing] 类中编写了 [run] 方法,抽象类 [AbstractElectionsSwing] 的 [main] 方法即可被移除。

10.3.6. 可执行类

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

我们希望 Spring 能像在控制台应用程序中实现时那样,实例化 [ui] 层。为此,实现类 [ElectionsSwing] 必须持有对 [business] 层的引用:


@Component
public class ElectionsSwing extends AbstractElectionsSwing implements IElectionsUI{
 
  // reference on the [business] layer
  @Autowired
  private IElectionsMetier metier;
...
  • 第 1 行:[ElectionsSwing] 类是 Spring 组件;
  • 第 5–6 行:Spring 将一个引用注入到 [业务] 层;

通过执行以下 [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] 类的无参构造函数;
  • 注入依赖项,本例中即注入对 [business] 层的引用;
  • 执行 [ElectionsSwing] 类的 [run] 方法:

    @Override
    public void run() {
        // on affiche l'interface graphique
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                init();
                setVisible(true);
            }
        });
}
  • 在第 12 行,我们指定将调用父类的 [init] 方法,该方法将绘制 GUI 组件。我们将在 [ElectionsSwing] 类中本地重写此方法。这次,我们将在该方法内使用数据初始化窗口组件(下拉列表框、标签):

本地 [init] 方法的骨架代码如下:


public class ElectionsSwing extends AbstractElectionsSwing implements IElectionsUI {
 
    ...
  // reference on the [business] layer
  @Autowired
  private IElectionsMetier metier;
 
    // initializations
    public void init() {
      // generation of components by the parent class
    super.init();
 
    // lists are requested from the [metier] layer
        ...
        // associate list names with the jComboBoxNomsListes combo
        ...
        // and election parameters
        ...
        // we initialize the labels linked to these two pieces of information
        ...
        // message of success
        ...
        // initialization status of certain components form
...
}

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

10.3.8. [Utilities] 类

[Utilities] 类中汇总了若干静态辅助方法:

 

[Utilities] 类的定义如下:


package istia.st.elections.ui;
 
import javax.swing.JLabel;
import javax.swing.JMenuItem;
 
//utility class
class Utilitaires {
    // manage label array status
    public static void setEnabled(JLabel[] labels, boolean value) {
        for (int i = 0; i < labels.length; i++) {
            labels[i].setEnabled(value);
        }
    }
 
    // manage the status of a table of menu options
    public static void setEnabled(JMenuItem[] menuItems, boolean value) {
        ...
    }
 
}
  • 第 9 行:setEnabled 方法用于设置数组中定义的 JLabel 组件的状态。JLabel 组件的 setEnabled 方法允许您启用或禁用该 JLabel

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


10.3.9. [ElectionsSwing] 类的代码

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


package istia.st.elections.ui;
 
...
public class ElectionsSwing extends AbstractElectionsSwing implements IElectionsUI {
 
  // reference on the [business] layer
  @Autowired
  private IElectionsMetier metier;
 
 
    // initializations
    public void init() {
    ...
    }
 
    // event managers
 
    @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] 方法的目标如下:

  • 将列表的 ID 和名称以 [ID - 名称] 的格式填充到下拉列表框 [4] 中
  • 在 [15] 处显示成功提示
  • 初始化标签 [2] 和 [3]
  • 禁用某些链接

[init] 方法的骨架代码如下:


@Override
    protected void init() {
        // génération des composants par la classe parent
        super.init();
        // initialisations locales
        modèleNomsVoix = new DefaultListModel<>();
        jListNomsVoix.setModel(modèleNomsVoix);
        modèleRésultats = new DefaultListModel<>();
        jListResultats.setModel(modèleRésultats);
        String info;
        try {
            // on demande les listes à la couche [métier]
            listes = ...
            // on associe les noms des listes au combo jComboBoxNomsListes
            ...
            // ainsi que les paramètres de l'election
            int nbSiegesAPourvoir = ...
            double seuilElectoral = ...
            // on initialise les labels liés à ces deux informations
            ...
            // message de succès
            info = "Source de données lue avec succès";
        } catch (ElectionsException ex1) {
            // on note l'error
            info = getInfoForException("Les erreurs suivantes se sont produites :", ex1);
        } catch (RuntimeException ex2) {
            // on note l'error
            info = getInfoForException("Les erreurs suivantes se sont produites :", ex2);
        }
        // on affiche l'info
        jTextPaneMessages.setText(info);
        jTextPaneMessages.setCaretPosition(0);
        // état formulaire
        Utilitaires.setEnabled(new JLabel[] { jLabelAjouter, jLabelCalculer, jLabelEnregistrer, jLabelSupprimer }, false);
        Utilitaires.setEnabled(
                new JMenuItem[] { jMenuItemAjouter, jMenuItemCalculer, jMenuItemEnregistrer, jMenuItemSupprimer }, false);
        // centrer la fenêtre
        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) {
        // on affiche le message
        StringBuffer info = new StringBuffer(String.format("%s -------------\n", message));
        info.append(String.format("Code erreur : %d\n", ex.getCode()));
        // on affiche les erreurs
        for (String erreur : ex.getErreurs()) {
            info.append(String.format("-- %s\n", erreur));
        }
        return info.toString();
    }
 
    private String getInfoForException(String message, RuntimeException ex) {
        // on affiche le message
        StringBuffer info = new StringBuffer(String.format("%s -------------\n", message));
        // on affiche la pile des exceptions
        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. 管理 [Add] 链接的状态

[添加]按钮[6]仅在[5]投票字段不为空时才处于活动状态。在[AbstractElectionsSwing]类中,用于跟踪[5]字段光标移动的处理程序如下:


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

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


    protected void doMajLabelAjouter() {
        // on fixe l'état du label [jLabelAjouter]
        ...
        // on fixe l'état du menu [jMenuItemAjouter]
        ...
}

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


10.3.9.3. 为每个列表分配票数

对于来自 (4) 的每个候选列表,请按以下步骤操作:

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

输入错误将按以下示例所示进行标记:

Image

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

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


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

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


  // modèles des listes JList
  private DefaultListModel<String> modèleNomsVoix = null;
  private DefaultListModel<String> modèleRésultats = null;
 
  // les listes en compétition
  private ListeElectorale[] listes;
 
  // listes saisies par l'user
  private final List<ListeElectorale> listesSaisies = new ArrayList<>();
  private ListeElectorale[] tListesSaisies;
...
  @Override
  protected void doAjouter() {
    // le nombre de voix est-il correct ?
    ...
    // si erreur, alors on la signale
    if (erreur) {
      JOptionPane.showMessageDialog(null, "Nombre de voix incorrect", "Elections : erreur",
              JOptionPane.INFORMATION_MESSAGE);
      jTextFieldVoixListe.requestFocus();
      // retour à l'graphic interface
      return;
    }
    // pas d'error - save the list
    listesSaisies.add(...);
    modèleNomsVoix.addElement(...);
    // on nettoie le nombre de voix
    jTextFieldVoixListe.setText("");
    // état formulaire (menus, labels)
...
}
  • 第 25 行:每次用户向列表中添加语音并确认选择时,该列表都会存储在第 9 行的 [listesSaisies] 字段中。该列表以 [id, version, name, voice] 的形式保存在此处。前三个信息来自第 6 行数组中最初存储的列表。下拉列表的 [getSelectedIndex] 方法返回所选列表的索引;

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


10.3.9.4. 管理 [Delete] 链接的状态

只有在 [8] 中选中了某项时,[Delete] 链接 [9] 才处于活动状态。

在 [AbstractElectionsSwing] 类中,响应列表 [8] 中项目点击事件的处理程序如下:


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

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


    @Override
    protected void doMajLabelSupprimer() {
            // on allume le label [jLabelSupprimer] et l'corresponding menu option
            ...
}

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


10.3.9.5. 删除候选人列表

[删除]链接[9]允许您删除在(8)中选中的(姓名,票数)对。删除完成后,[删除]链接将被禁用。只有当在(8)中选中新的列表时,该链接才会再次启用。

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


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

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


@Override
    protected void doSupprimer() {
        // suppression de la liste sélectionnée, du modèle modèleNomsVoix et des listes saisies
        ...
        // maj de l'status of labels and form menu options
        Utilitaires.setEnabled(...);
    ...
}

待完成工作:编写 [doSupprimer] 方法的代码。


10.3.9.6. 管理 [Calculate] 链接的状态

只有当 [8] 中至少有一个项目时,[Calculate] 链接 [10] 才处于活动状态。


任务:在之前编写的 [doAdd] 和 [doDelete] 方法中添加必要的代码来处理此链接。相应的菜单选项也将一并处理。


注意DefaultListModel 中的项目数量可通过 size() 方法获取。

10.3.9.7. 计算座位数

通过[计算]链接[10],您可以开始座位计算,并在(14)处显示结果。如果计算失败(所有列表均已被排除),则会在[15]处显示错误信息。无论结果如何,计算完成后,[计算]链接[10]都会被禁用。

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


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

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


  // listes saisies par l'utilisateur
  private final List<ListeElectorale> listesSaisies = new ArrayList<>();
  private ListeElectorale[] tListesSaisies;
 
...
  @Override
  protected void doCalculer() {
    tListesSaisies = listesSaisies.toArray(new ListeElectorale[0]);
    // calcul des sièges
    try {
      ...
    } catch (ElectionsException ex) {
      // on affiche l'exception
      ...
      return;
    }
    // affichage des résultats
    ...
    // maj état formulaire
    Utilitaires.setEnabled(...);
}

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


10.3.9.8. 将结果保存到数据源

[保存] 链接 (12) 允许您将座位计算结果保存到数据源。保存成功后,[保存] 链接将被禁用。如果保存失败,[15] 处将显示一条错误消息。无论哪种情况,[保存] 链接随后都会被禁用。

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


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

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


@Override
    protected void doEnregistrer() {
        // on demande l'enregistrement à la couche métier
        try {
            ...
        } catch (ElectionsException ex) {
            // on affiche l'exception
            ...
            // retour à l'interface graphique
            return;
        }
        // maj du formulaire
        Utilitaires.setEnabled(...);
...
}

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


10.3.9.9. 清除结果

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

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


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

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


    @Override
    protected void doEffacer() {
        // on vide la liste des résultats
        ....
        // maj du formulaire
        Utilitaires.setEnabled(...);
}

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


注意DefaultListModel 类有一个 clear() 方法,用于移除所有元素。

10.3.10. 改进

之前的图形界面可以通过多种方式进行改进:用户可能会忘记为组合框中所有列表输入投票,也可能不小心对同一个列表重复投票。


任务:改进算法,确保上述两种情况均不会发生。一个简单的解决方案是维护一个已输入列表的字典,其中键为下拉列表框中的项目。我们还将确保只有在所有列表均已输入后,[计算] 按钮才会启用。


参见课程[ref1]:第3.8节中的HashTable类。