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






4.67/5 (3投票s)
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 的,都非常有帮助并在本程序中使用。