数独GUI
好的,我忍不住了……这是我的尝试。它全部在一个包中:
- 符合规范的所有元素的GUI(问题)
- 灵活的布局
- 没有外部依赖-使用标准Swing布局
- 输入验证(仅限数字0-9)
- 模型视图控制器架构
- 后台任务运行程序(您的GUI永远不会冻结)
- 内置一些调试方法(将数独输出为文本)
- 虚拟实现-模拟长时间运行的计算,显示GUI的响应能力
我尽力让代码尽可能易读。可能有一些不太清晰的部分。可能线程部分不太清晰,但如果有人发现它有用,我很乐意更好地描述它。
所以我的目标是尽可能简单的使用。如果您查看接口,很难破坏这些东西(冻结UI,获取空指针等),作为编写公共API的练习。这可能不是最佳实现,但这是我编写的最好之一。 :)
希望对您有所帮助。
这是它的外观:
(注意:值是随机的)
用法
您所需要做的就是实现接口:
public interface SudokuImplementation {
void goButtonPressed(Integer[][] leftSudokuValues, SudokuController resultAcceptor);
}
在这个方法中进行所有计算,并使用resultAcceptor.setSudokuResult()
存储结果。
以下是如何实际显示GUI:
SudokuImplementation sudokuImplementation =
new YourSuperSudoku(); // <- your implementation
SudokuView sudokuView = new SudokuView();
sudokuView.setSudokuImplementation(sudokuImplementation);
sudokuView.setVisible(true);
And that's all!
代码
所有类都在默认包中 - 您可以根据需要进行重构。以下是它们的列表:
- SudokuView - 主要GUI
- SudokuRun - 示例运行器
- SudokuController - 允许以安全方式控制视图
- SudokuImplementation - 数独实现的接口
- DummySudokuImplementation - 示例实现
1. SudokuView:
import java.awt.*;
import javax.swing.*;
import javax.swing.text.*;
import javax.swing.border.*;
public class SudokuView extends JFrame {
SudokuController controller;
public void setSudokuImplementation(SudokuImplementation listener) {
controller.setListener(listener);
}
public SudokuView() {
controller = new SudokuController();
setTitle("Sudoku Solver 1.0");
getContentPane().add(createCenterPanel(), BorderLayout.CENTER);
getContentPane().add(createBottomPanel(), BorderLayout.SOUTH);
setMinimumSize(new Dimension(600, 300));
pack();
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
private JPanel createBottomPanel() {
JPanel bottomPanel = new JPanel(new GridBagLayout());
JLabel leftLabel = createLabel("left");
JLabel rightLabel = createLabel("right");
controller.bindLeftLabel(leftLabel);
controller.bindRightLabel(rightLabel);
bottomPanel.add(leftLabel, getWholeCellConstraints());
bottomPanel.add(new JSeparator(JSeparator.VERTICAL));
bottomPanel.add(rightLabel, getWholeCellConstraints());
bottomPanel.setBorder(new BevelBorder(BevelBorder.LOWERED));
return bottomPanel;
}
private JLabel createLabel(String text) {
JLabel label = new JLabel(text);
label.setHorizontalAlignment(JLabel.CENTER);
return label;
}
private JPanel createCenterPanel() {
JPanel centerPanel = new JPanel(new GridBagLayout());
centerPanel.add(createLeftPanel(), getWholeCellConstraints());
centerPanel.add(createCenterButton(), getPreferredSizeConstraint());
centerPanel.add(createRightPanel(), getWholeCellConstraints());
return centerPanel;
}
private GridBagConstraints getPreferredSizeConstraint() {
return new GridBagConstraints();
}
private JButton createCenterButton() {
JButton goButton = new JButton(">");
controller.bindCenterButton(goButton);
return goButton;
}
private static final Insets sixPixelInset = new Insets(6, 6, 6, 6);
private JPanel createRightPanel() {
JPanel rightPanel = create3x3Panel(6);
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
JPanel panel2 = create3x3Panel(2);
fillPanelWithNonEditable(panel2, i, j);
rightPanel.add(panel2);
}
}
rightPanel.setBorder(new EmptyBorder(sixPixelInset));
return rightPanel;
}
private JPanel createLeftPanel() {
JPanel leftPanel = create3x3Panel(6);
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
JPanel panel2 = create3x3Panel(2);
fillPanelWithEditable(panel2, i, j);
leftPanel.add(panel2);
}
}
leftPanel.setBorder(new EmptyBorder(sixPixelInset));
return leftPanel;
}
private GridBagConstraints getWholeCellConstraints() {
GridBagConstraints wholePanelCnstr = getPreferredSizeConstraint();
wholePanelCnstr.fill = java.awt.GridBagConstraints.BOTH;
wholePanelCnstr.weightx = 1.0;
wholePanelCnstr.weighty = 1.0;
return wholePanelCnstr;
}
private void fillPanelWithEditable(JPanel panel, int majorRow, int majorColumn) {
for (int minorRow = 0; minorRow < 3; minorRow++) {
for (int minorColumn = 0; minorColumn < 3; minorColumn++) {
final JFormattedTextField editableField = createEditableField();
int column = majorColumn * 3 + minorColumn;
int row = majorRow * 3 + minorRow;
controller.bindLeftSudokuCell(row, column, editableField);
panel.add(editableField);
}
}
}
private void fillPanelWithNonEditable(JPanel panel, int majorRow, int majorColumn) {
for (int minorRow = 0; minorRow < 3; minorRow++) {
for (int minorColumn = 0; minorColumn < 3; minorColumn++) {
final JFormattedTextField editableField = createNonEditableField();
int column = majorColumn * 3 + minorColumn;
int row = majorRow * 3 + minorRow;
controller.bindRightSudokuCell(row, column, editableField);
panel.add(editableField);
}
}
}
private JPanel create3x3Panel(int gap) {
final GridLayout gridLayout = new GridLayout(3, 3, 1, 1);
gridLayout.setHgap(gap);
gridLayout.setVgap(gap);
JPanel panel = new JPanel(gridLayout);
return panel;
}
private JFormattedTextField createNonEditableField() {
JFormattedTextField field = createEditableField();
field.setEditable(false);
field.setBackground(Color.WHITE);
return field;
}
private JFormattedTextField createEditableField() {
JFormattedTextField field = new JFormattedTextField();
try {
field.setFormatterFactory(new DefaultFormatterFactory(new MaskFormatter("#")));
} catch (java.text.ParseException ex) {
}
field.setPreferredSize(new Dimension(16, 30));
field.setHorizontalAlignment(javax.swing.JTextField.CENTER);
field.setText(" ");
field.setBorder(null);
return field;
}
}
2. SudokuRun:
import java.awt.EventQueue;
import javax.swing.UIManager;
public class SudokuRun implements Runnable {
public void run() {
SudokuImplementation sudokuImplementation = new DummySudokuImplementation();
SudokuView sudokuView = new SudokuView();
sudokuView.setSudokuImplementation(sudokuImplementation);
sudokuView.setVisible(true);
}
public static void main(String args[]) {
tryToSetSystemLookAndFeel();
EventQueue.invokeLater(new SudokuRun());
}
private static void tryToSetSystemLookAndFeel() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception ex) {
System.out.println("Couldn't set LAF");
}
}
}
3. SudokuController:
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.JButton;
import javax.swing.JFormattedTextField;
import javax.swing.JLabel;
public class SudokuController {
JLabel leftLabel, rightLabel;
JFormattedTextField[][] leftSudoku, rightSudoku;
JButton goButton;
public SudokuController() {
leftSudoku = new JFormattedTextField[9][9];
rightSudoku = new JFormattedTextField[9][9];
}
void bindLeftLabel(JLabel label) {
leftLabel = label;
}
void bindRightLabel(JLabel label) {
rightLabel = label;
}
void bindLeftSudokuCell(final int row, final int column, JFormattedTextField field) {
field.addPropertyChangeListener("value", new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
if (evt.getNewValue() != null) {
String newValue = (String) evt.getNewValue();
userEditedValueAt(row, column, Integer.valueOf(newValue));
}
}
});
leftSudoku[row][column] = field;
}
void userEditedValueAt(int row, int column, int value) {
System.out.println("Value changed at row:" + row + ", column:" + column + " to " + value);
}
void bindRightSudokuCell(int row, int column, JFormattedTextField field) {
rightSudoku[row][column] = field;
}
void spitOutSudokus() {
System.out.println("Left:");
System.out.println(getPrettyPrinted(leftSudoku));
System.out.println("Right:");
System.out.println(getPrettyPrinted(rightSudoku));
}
private String getPrettyPrinted(JFormattedTextField[][] sudoku) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 9; i++) {
sb.append("|");
for (int j = 0; j < 9; j++) {
if (sudoku[i][j] != null) {
sb.append(sudoku[i][j].getText());
} else {
sb.append("-");
}
sb.append(" ");
}
sb.append("|\n");
}
return sb.toString();
}
void bindCenterButton(JButton goButton) {
this.goButton = goButton;
goButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
goButtonPressed();
}
});
}
SudokuImplementation listener;
public void setListener(SudokuImplementation listener) {
this.listener = listener;
}
Thread backGroundThread;
private void goButtonPressed() {
if (listener != null) {
if (backGroundThread == null || (backGroundThread != null && !backGroundThread.isAlive())) {
backGroundThread = new Thread() {
@Override
public void run() {
listener.goButtonPressed(getLeftValues(), SudokuController.this);
}
};
backGroundThread.start();
}
}
}
private Integer[][] getLeftValues() {
Integer[][] values = new Integer[9][9];
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if (!leftSudoku[i][j].getText().equals(" ")) {
values[i][j] = Integer.valueOf(leftSudoku[i][j].getText());
}
}
}
return values;
}
public void setSudokuResult(final Integer[][] result) {
EventQueue.invokeLater(new Runnable() {
public void run() {
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
rightSudoku[i][j].setValue(String.valueOf(result[i][j]));
}
}
}
});
}
public void setSudokuTime(final String time) {
EventQueue.invokeLater(new Runnable() {
public void run() {
leftLabel.setText("<html>Running time: <b>" + time);
}
});
}
public void setSudokuCompleted(final boolean completed) {
EventQueue.invokeLater(new Runnable() {
public void run() {
rightLabel.setText("<html>Completely Solved: <b>" + completed);
if (completed) {
spitOutSudokus();
}
}
});
}
}
4. 数独实现:
public interface SudokuImplementation {
void goButtonPressed(Integer[][] leftSudokuValues, SudokuController resultAcceptor);
}
5. DummySudokuImplementation:
import java.util.concurrent.TimeUnit;
class DummySudokuImplementation implements SudokuImplementation {
public DummySudokuImplementation() {
}
public void goButtonPressed(Integer[][] leftSudokuValues, SudokuController resultAcceptor) {
System.out.println("Long running computation simulation...");
for (int i = 0; i < 50; i++) {
resultAcceptor.setSudokuCompleted(false);
resultAcceptor.setSudokuTime(String.valueOf(i * 50) + "ms");
resultAcceptor.setSudokuResult(getRandomResult());
waitSomeTime();
}
resultAcceptor.setSudokuResult(leftSudokuValues);
resultAcceptor.setSudokuCompleted(true);
waitSomeTime();
System.out.println("Done!");
}
private void waitSomeTime() {
try {
TimeUnit.MILLISECONDS.sleep(50);
} catch (InterruptedException ex) {
}
}
private Integer[][] getRandomResult() {
Integer[][] randomResult = new Integer[9][9];
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
randomResult[i][j] = (int) (Math.random() * 9);
}
}
return randomResult;
}
}
解释
我并不认为我的做法是最好的。我很想看到其他答案,比如所有视图都使用MigLayout完成。这将非常有教育意义。当Sun的实现只有一个时,我正在学习Swing GUI,所以它在我的风格中占主导地位。话虽如此,我建议咨询Sun的Swing GUI短期课程。它还包括一个简单的案例研究。阅读完后,几乎整个SudokuView的大部分都应该清晰明了。
我确实将代码分离以使其更易读。这就是为什么控制器是另一个类而不是视图的一部分。视图仅用于构建小部件和布局,但为了简单起见(不创建更多类),我也在其中初始化控制器。
真正的工作在控制器中。它包含最复杂的细节...线程也在那里,因此它实际上是做了些什么并不那么明显。我从头开始实现了一个Thread类。有一个替代方案:使用SwingWorker。这可能是一个陈词滥调,但要明确:我使用线程使GUI随时响应。如果没有适当的线程处理,整个GUI将在计算时冻结。我决定从Sudoku的实现角度尽可能地简化它,如非阻塞增量更新。
至于线程,了解哪些代码运行在哪个线程非常重要。由GUI组件触发的每个操作都在EDT(事件分派线程)上运行。如果您在其中执行任何长时间运行的任务,则GUI将不会响应。因此,我只需创建另一个线程(请参见goButtonPressed()
的实现)并启动它。之后,EDT可以处理任何其他事件而不会阻塞。
因此,您的Sudoku在一个特殊的后台线程中运行。除非它必须更新GUI,否则它可以随意进行任何操作。它几乎肯定会这样做,因为这就是部分更新的位置。这里有一个问题:如果您直接调用任何GUI组件(设置某些值),那么GUI将被冻结。这是一种称为EDT分派违规的条件。为避免任何冻结,所有与Swing的交互都应在EDT上完成。如何做到这一点?EDT有一个特殊的事件队列,专门用于此。您可以在队列上发布一个更新事件。在EDT上,代码会不断地监视传入事件并相应地更新GUI。因此,基本上它是后台线程和EDT之间的通信。要在队列上发布事件,您可以使用专门为此设计的特殊实用程序方法:EventQueue.invokeLater(new Runnable() { /* 这里放置您的GUI交互 */ });
。请查看SudokuController
方法:
- setSudokuResult()
- public void setSudokuTime()
- setSudokuCompleted()
这就是GUI更新事件发布的位置。