65.9K
CodeProject 正在变化。 阅读更多。
Home

构建数据模型、视图和控制

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (3投票s)

2014年3月26日

CPOL

4分钟阅读

viewsIcon

12512

downloadIcon

351

JNotif - Java 通知应用的示例

引言

基于我在软件工程课程中的“良知”项目,以及最近的个人项目'TodayThought',这个 JNotif 的想法是让用户保存(重要或喜欢的)笔记、引言或消息。 然后,用户可以选择如何以及何时提醒他们这些信息

  • 在计算机屏幕上显示这些消息
  • 可以重新定位和调整此区域的大小
  • 大小和位置会被记录下来,并在下次程序启动时恢复
  • 可以设置字体系列、字体大小、前景色和背景色
  • 安排显示时间
  • 可选的音频通知

计划

首先创建数据模型,因为它是整个程序的基础和繁重的工作。 然后是数据的 GUI 视图,以及操作数据和设置视图的控制。

1. 数据模型

本程序中使用 XML。 我们需要以下内容

popups.xml

popups.xml 用于存储所有消息、引言或笔记。 这些项目中的每一个都是一个弹出窗口。 结构如下

<?xml version="1.0" encoding="utf-8"?>
<popups>
    <popup id="1">
        <text>Lorem ipsum, dolor sit amet, nullam wisi scelerisque velit.</text>
    
    <popup id="2">
        <text>Ac ante pellentesque, erat venenatis gravida mauris felis id id.</text>
    </popup>    
        ...

我们将使用三个类来处理这个 popups.xml:Popup、Popups 和 PopupParser。

Popup.class 用于对单个弹出窗口进行建模

import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name = "popup")
public class Popup implements {

    private int id;
    private String text;

    ... Regular constructors and getters 

    @XmlAttribute(name = "id")
    public void setId(int id) {
        this.id = id;
    }

    @XmlElement(name = "text")
    public void setText(String text) {
        this.text = text;
    }
}

Popups 类用于对弹出窗口列表进行建模

import java.util.ArrayList;
import java.util.Random;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name = "popups")
public class Popups {

    private ArrayList<Popup> popups;

    public ArrayList<Popup> getPopups() {
        return this.popups;
    }

    public int getSize() {
        return this.popups.size();
    }

    @XmlElement(name = "popup")
    public void setPopups(ArrayList<Popup> popups) {
        this.popups = popups;
    }

    public Popup getPopup(int index) {
        return this.popups.get(index);
    }

    public Popup getRandomPopup() {
        Random random = new Random();
        int randomNum = random.nextInt(this.getSize());
        return this.getPopup(randomNum);
    }

    public void addPopup(Popup popup) {
        //popup.setId(this.popups.get(this.popups.size() - 1).getId() + 1);
        this.popups.add(popup);
        this.reIndex();
    }

    // Remove and re-index popups
    public void removePopup(int index) {
        this.popups.remove(index);
        this.reIndex();
    }

    public void removeLastPopup() {
        this.removePopup(this.popups.size() - 1);
    }

    public void removeFirstPopup() {
        this.removePopup(0);
    }

    private void reIndex() {
        for (int i = 1; i <= this.popups.size(); i++) {
            this.popups.get(i - 1).setId(i);
        }
    }
}

PopupParser 用于解析来自 popups.xml 的 XML 数据,并将数据写回该文件。

import java.io.File;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;

public class PopupParser {

    private Popups popups;
    private int popupIndex;
    File file;

    public PopupParser(String file) throws JAXBException {
        this.file = new File(file);
        this.readPopups();
    }

    // Read list of popups from popups.xml
    private void readPopups() throws JAXBException {
        JAXBContext jAXBContext = JAXBContext.newInstance(Popups.class);

        Unmarshaller unmarshaller = jAXBContext.createUnmarshaller();
        this.popups = (Popups) unmarshaller.unmarshal(this.file);
    }

    // Write popups to popups.xml
    public void writePopupsToXMLFile(Popups popups) throws JAXBException {
        JAXBContext jAXBContext = JAXBContext.newInstance(Popups.class);
        Marshaller marshaller = jAXBContext.createMarshaller();

        // Output pretty format
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(popups, this.file);
    }

    public int getNumPopups() {
        return this.popups.getSize();
    }

    public Popup getPopup(int index) {
        return this.popups.getPopup(index);
    }

    public Popup getNextPopup() {
        if (this.popupIndex >= this.popups.getSize()) {
            this.popupIndex = 0;
        }
        return this.popups.getPopup(popupIndex++);
    }

    public Popup getPrevPopup() {
        if (this.popupIndex <= 0) {
            this.popupIndex = this.popups.getSize() - 1;
        }
        return this.popups.getPopup(popupIndex--);
    }

    public Popup getRandomPopup() {
        return this.popups.getRandomPopup();
    }

    public Popups getPopups() {
        return this.popups;
    }
}

themes.xml

此 XML 用于存储主题的设置。 结构如下

<?xml version="1.0" encoding="utf-8"?>
<themes>
    <theme id="1">
        <audio_alert>Windows Information Bar.wav</audio_alert>
        <msg_format>
            <color_background>#333333</color_background>
            <font_family>Tahoma</font_family>
            <font_size>14</font_size>
            <font_size_title_bar>8</font_size_title_bar>
            <position>bottom, right</position>
            <color_text>#FFFFFF</color_text>
        </msg_format>
        <name>Default</name>
    
    
    ... More themes

我们注意到,由于此 XML 的“嵌套”结构,为了对主题进行建模,我们使用一个额外的类来表示消息格式。 因此,除了名称和音频警报之外,Theme 类还使用 MsgFormat 作为其数据成员。 MsgFormat 类的定义是

// Import packages

public class MsgFormat {

    private String fontFamily;
    private int msgFontSize;
    private int msgFontTitleSize;
    private Color bgColor;
    private Color textColor;
    private String msgPosition;

    public MsgFormat(String fontFamily, int msgFontSize,
            Color gbColor, Color textColor) {
        this(fontFamily, msgFontSize, 8, gbColor, textColor, "bottom, right");
    }

    public MsgFormat(String fontFamily, int msgFontSize, int msgFontTitleSize,
            Color gbColor, Color textColor, String msgPosition) {
        this.fontFamily = fontFamily;
        this.msgFontSize = msgFontSize;
        this.msgFontTitleSize = msgFontTitleSize;
        this.bgColor = gbColor;
        this.textColor = textColor;
        this.msgPosition = msgPosition;
    }

    @XmlElement(name = "font_size_title_bar")
    public void setMsgFontTitleSize(int msgFontTitleSize) {
        this.msgFontTitleSize = msgFontTitleSize;
    }

    @XmlJavaTypeAdapter(ColorAdapter.class)
    public Color getBgColor() {
        return bgColor;
    }

    ... Getters and Setters here
}

ColorAdapter 类用于根据需要在 String 和 Color 之间进行转换

import java.awt.Color;
import javax.xml.bind.annotation.adapters.XmlAdapter;

public class ColorAdapter extends XmlAdapter<String, Color> {

    @Override
    public Color unmarshal(String s) {
        return Color.decode(s);
    }

    @Override
    public String marshal(Color color) {
        String hex = Integer.toHexString(color.getRGB()).toUpperCase();
        hex = hex.substring(2, hex.length());

        return '#' + hex;
    }
}

现在回到 Theme 类

... Import packages here

@XmlRootElement(name = "theme")
public class Theme implements Cloneable {

    private int id;
    private String name;
    private String audioAlert;
    private MsgFormat msgFormat;

    public Theme(String name, String audio, MsgFormat format) {
        this.audioAlert = audio;
        this.name = name;
        this.msgFormat = format;
    }

    public int getId() {
        return id;
    }

    ... Similiar Getters and Setters here

    public MsgFormat getFormat() {
        return msgFormat;
    }

    @XmlElement(name = "msg_format")
    public void setFormat(MsgFormat msgFormat) {
        this.msgFormat = msgFormat;
    }
}

Themes 和 ThemeParser 与上面的 Popups 和 PopupParser 类似。

config.xml

此 XML 用于保存配置设置

<?xml version="1.0" encoding="utf-8"?>
<config>
    <audio_volume>50.0
    <display_time>3</display_time>
    <frequency>3</frequency>
    <em>true</em>

因为只有一个配置,所以我们不需要 Configs.class。 Config 和 ConfigParser 这两个类以与上述相同的方式实现。

2. 视图

一个对话框用于显示单个弹出窗口

除了 GUI(通过 Netbeans IDE 实现)和线程(为了提高响应速度)之外,还实现了一些功能

  • 此对话框是可移动和可调整大小的
  • 位置和大小被序列化为一个属性文件。 因此,程序会记住这些设置以供以后使用。

这两种方法用于存储和检索此弹出对话框在屏幕上的位置和大小

// Store and Restore last position of dialog/frame
    public void storeOptions(JDialog dialog) throws IOException {
        File file = new File(this.locPropertyFile);
        Properties properties = new Properties();

        Rectangle r = dialog.getBounds();
        int x = (int) r.getX();
        int y = (int) r.getY();
        int w = (int) r.getWidth();
        int h = (int) r.getHeight();

        properties.setProperty("x", "" + x);
        properties.setProperty("y", "" + y);
        properties.setProperty("w", "" + w);
        properties.setProperty("h", "" + h);

        BufferedWriter br = new BufferedWriter(new FileWriter(file));
        properties.store(br, "Properties of the user frame");
    }

    public void restoreOptions(JDialog dialog) throws IOException {
        File file = new File(this.locPropertyFile);
        Properties p = new Properties();
        BufferedReader br = new BufferedReader(new FileReader(file));
        p.load(br);

        int x = Integer.parseInt(p.getProperty("x"));
        int y = Integer.parseInt(p.getProperty("y"));
        int w = Integer.parseInt(p.getProperty("w"));
        int h = Integer.parseInt(p.getProperty("h"));

        Rectangle r = new Rectangle(x, y, w, h);
        dialog.setBounds(r);
    }

除此之外,还有其他 GUI,例如弹出窗口管理器、主题管理器和设置框架/面板,让用户管理(删除、更新、保存弹出窗口、主题和设置)。 不确定这些 GUI 在 MVC 实践中属于“视图”还是“控制”。 无论如何,这无关紧要。

设置面板

主题管理器框架

弹出窗口管理器框架

3. 控制

程序的入口点,ControlNotif 类,以及程序的系统托盘被设计为本程序中的“控制”角色。 ControlNotif 首先创建所有必要的东西(数据、组件、对象):系统托盘、框架/面板/对话框等。
程序的系统托盘

// System Tray
        sysTray = new SystemTrayNotif("images/quotes-icon.png", this.popupParser,
                this.themeParser, this.configParser, this.dialogNotif, this);
        Thread threadSysTray = new Thread(sysTray);
        threadSysTray.start();

// Themes manager frame
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                frameThemeManager = new FrameThemesManager(themeParser);
            }
        });

此循环用于根据用户设置显示弹出窗口

// Notification
        while (true) {
            if (this.isPaused == false) {

                // Play audio
                if (this.isAudioEnable) {
                    AudioNotif audioNotif = new AudioNotif(this.theme.getAudioAlert(),
                            this.config.getAudioVolume());
                    Thread audioThread = new Thread(audioNotif);
                    audioThread.start();
                }

                // Show DialogNotif
                new DialogNotif(this.popupParser,
                        this.theme, this.config).showPopup();

                Thread.sleep(this.config.getFreq() * 1000);
            } else {
                Thread.sleep(this.config.getFreq() * 1000);
            }
        }

可选的音频通知由 AudioNotif 类实现

... Import needed packages here

public class AudioNotif implements Runnable {

    String audioFolder = "audio";
    String audioFile = "";
    double volume = 50.0;

    public AudioNotif(String fileName, double vol) {
        this.audioFile = this.audioFolder + "/" + fileName;
        this.volume = vol;
    }

    @Override
    public void run() {
        try {
            AudioInputStream audioInputStream
                    = AudioSystem.getAudioInputStream(new File(this.audioFile));

            AudioFormat baseFormat = audioInputStream.getFormat();
            AudioFormat decodedFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED,
                    baseFormat.getSampleRate(), 16, baseFormat.getChannels(),
                    baseFormat.getChannels() * 2, baseFormat.getSampleRate(), false);
            try (AudioInputStream decodedAs = AudioSystem.getAudioInputStream(decodedFormat, audioInputStream); Clip clip = AudioSystem.getClip()) {
                clip.open(decodedAs);

                FloatControl gainControl = (FloatControl) clip.getControl(FloatControl.Type.MASTER_GAIN);

                this.volume *= 0.01; // Min 0 to Max 1.0
                float dB = (float) (Math.log(this.volume) / Math.log(10.0) * 20.0);
                gainControl.setValue(dB);

                clip.start();

                do {
                    Thread.sleep(100); // To have time to play
                } while (clip.isRunning());

                clip.stop();
            }

        } catch (UnsupportedAudioFileException | IOException | LineUnavailableException | InterruptedException ex) {
            Logger.getLogger(AudioNotif.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}

总结和未来发展

我认为这个程序是有用且有趣的,对于需要被提醒的忙碌用户,或者对于任何只想存储他们喜欢的弹出窗口(笔记、引言或消息)并在他们的桌面上以所需的频率、时间间隔和自定义设置(例如位置、外观和感觉)显示它们的用户。

这就是为什么我计划进一步开发这个程序。 由于 XML 的限制,将使用嵌入式 Java DB (Derby) 来存储用户弹出窗口和设置的关系数据库。
接下来可能是更多可自定义的主题、主题包、Web 集成和社交功能。
本文的源代码目前不可用,因为它正在使用 Derby 进行修改和测试。

更新将在 JNotif 网站上提供。

致谢

这个程序是受到我在软件工程课程中的一个项目的启发,所有的 XML 解析都是我的团队成员之一 Kevin Schuck 完成的。
在开发这个程序的过程中,许多代码示例、教程和问答,特别是来自 StackOverlow 和 Oracle Tutorials 的,都非常有帮助并在本程序中使用。

© . All rights reserved.