Java Swing交易市場
- 結構
- UI界面
-
- 元件
-
- 文本輸入框
- 按鈕
- 比較常用的元件
- 動畫和繪畫
- 界面建構
-
- 簡單界面布局
- 複雜界面布局(GridBagLayout)
- 連接配接伺服器
-
- servlet連接配接
- socket
- 項目連結
結構
C/S結構進行開發,将用戶端UI等設計與後端伺服器等分離開,分别進行開發
- C/S結構
- MVC結構
- Socket和Servlet并用
UI界面
元件
文本輸入框
交易市場比較常用的元件之一就是文本輸入框,用于使用者名的輸入,密碼的輸入,或者商品搜尋功能
/**
* @Description 文字輸入框
* @author 廣闊大世界
* @Date 2020/3/31
*/
public class TextEdit extends JPanel {
protected final Color errorColor = MaterialColors.COSMO_RED;
protected final Color focusColor = MaterialColors.COSMO_BLUE;
protected final Color normalColor = MaterialColors.COSMO_DARK_GRAY;
protected int hintTextSize = 12;
protected int column = 22;
//繪制時的顔色,和計時器結合使用
protected Color paintColor;
//行尾圖示
protected ImageIcon endImage;
protected boolean focused;
//提示文字
protected String hintText;
//出錯文字,不為空則改變輸入框顔色
protected String errorText;
//文字輸入框本體
protected JTextField textField;
//行尾圖示的容器,可以增加監聽
protected JLabel endLabel;
//動畫計數
protected int num = 0;
//動畫分割數量
protected int max = 10;
protected Timer timer;
public TextEdit() {
this("");
}
public TextEdit(String hint){
super();
this.hintText = hint;
initial();
}
protected void initial(){
if (this.textField == null){
this.textField = new JTextField();
}
this.setLayout(new FlowLayout(FlowLayout.LEFT));
this.add(textField);
this.textField.setColumns(column);
this.textField.setBackground(null);
this.textField.setBorder(null);
this.textField.setFont(new Font("宋體",Font.BOLD,(int)(hintTextSize * 1.5f)));
this.setBorder(new EmptyBorder(hintTextSize + 1,0,hintTextSize,0));
//設定監聽,在擷取焦點之後重繪顔色
this.textField.addFocusListener(new FocusListener() {
@Override
public void focusGained(FocusEvent e) {
TextEdit.this.focused = true;
if (timer != null && timer.isRunning()){
timer.stop();
}
timer = new Timer(20,new GetFocusedListener());
timer.start();
}
@Override
public void focusLost(FocusEvent e) {
TextEdit.this.focused = false;
if (timer != null && timer.isRunning()){
timer.stop();
}
timer = new Timer(10,new LoseFocusListener());
timer.start();
}
});
//繪制行尾圖示
endLabel = new JLabel(endImage);
this.add(endLabel);
}
@Override
public void paint(Graphics g) {
super.paint(g);
Graphics2D g2=(Graphics2D) g;
int h = this.getHeight();
int w = this.textField.getWidth();
//設定繪圖品質
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
//繪制下劃線
if (errorText != null && errorText.length() > 0){
paintColor = errorColor;
} else if (num == max){
paintColor = focusColor;
} else if (num == 0){
paintColor = normalColor;
}
g2.setColor(paintColor);
g2.setStroke(new BasicStroke(1.5f));
g2.drawLine(0,h- hintTextSize - 1,w + 36,h - hintTextSize - 1);
//下劃線繪制結束
Paint textPaint;
//繪制錯誤資訊(不為空時)
if (errorText != null && errorText.length() > 0){
textPaint = new GradientPaint(0,0,errorColor,0,0,errorColor);
g2.setPaint(textPaint);
Font font = new Font("微軟雅黑",Font.PLAIN,hintTextSize - 1);
g2.setFont(font);
g2.setStroke(new BasicStroke(1));
g2.drawString(errorText,0,h - 2);
}
//繪制提示
if (hintText != null && hintText.length() > 0){
if (focused){
textPaint = new GradientPaint(0,0,focusColor,0,0,focusColor);
} else if (textField.getText() != null && textField.getText().length() > 0){
textPaint = new GradientPaint(0,0,MaterialColors.GRAY_700,0,0,MaterialColors.GRAY_700);
} else {
textPaint = new GradientPaint(0,0,normalColor,0,0,normalColor);
} if (errorText != null && errorText.length() > 0){
textPaint = new GradientPaint(0,0,errorColor,0,0,errorColor);
}
Font font = new Font("微軟雅黑",Font.PLAIN,hintTextSize);
g2.setPaint(textPaint);
g2.setFont(font);
g2.setStroke(new BasicStroke(1));
g2.drawString(hintText,0,hintTextSize);
}
g2.dispose();
}
//設定初始文本
public void setText(String text){
this.textField.setText(text);
repaint();
}
public String getText(){
return this.textField.getText();
}
public String getHintText() {
return hintText;
}
public void setHintText(String hintText) {
this.hintText = hintText;
repaint();
}
public String getErrorText() {
return errorText;
}
/**
* @Description 設定錯誤資訊,設為空或者""則不顯示
* @param errorText 出錯時的文本
* @return void
* @author 廣闊大世界
* @Date 2020/3/31
*/
public void setErrorText(String errorText) {
this.errorText = errorText;
repaint();
}
public int getColumn() {
return column;
}
public void setColumn(int column) {
this.column = column;
this.textField.setColumns(column);
repaint();
}
public ImageIcon getEndImage() {
return endImage;
}
public void setEndImage(ImageIcon endImage) {
this.endImage = endImage;
this.endLabel.setIcon(this.endImage);
repaint();
}
public JLabel getEndLabel() {
return endLabel;
}
//擷取焦點後的行為
protected class GetFocusedListener implements ActionListener{
@Override
public void actionPerformed(ActionEvent e) {
//繪制随時間變化的顔色
double individualR = ( 1.0 * focusColor.getRed() - normalColor.getRed()) / max;
double individualG = ( 1.0 * focusColor.getGreen() - normalColor.getGreen()) / max;
double individualB = ( 1.0 * focusColor.getBlue() - normalColor.getBlue()) / max;
num++;
if (num <= max){
paintColor = new Color(normalColor.getRed() + (int)individualR * num,
normalColor.getGreen() + (int)individualG * num,
normalColor.getBlue() + (int)individualB * num
);
} else {
timer.stop();
}
repaint();
}
}
//失去焦點後的行為
protected class LoseFocusListener implements ActionListener{
@Override
public void actionPerformed(ActionEvent e) {
//繪制随時間變化的顔色
double individualR = ( 1.0 * focusColor.getRed() - normalColor.getRed()) / max;
double individualG = ( 1.0 * focusColor.getGreen() - normalColor.getGreen()) / max;
double individualB = ( 1.0 * focusColor.getBlue() - normalColor.getBlue()) / max;
num--;
if (num >= 0){
paintColor = new Color(normalColor.getRed() + (int)individualR * num,
normalColor.getGreen() + (int)individualG * num,
normalColor.getBlue() + (int)individualB * num
);
} else {
timer.stop();
}
repaint();
}
}
}
這裡的文本輸入框實作了懸浮标題,以及錯誤提示,在擷取到焦點之後和失去焦點之後會有一段動畫效果,界面效果還可以
按鈕
按鈕也是很常用的元件,計劃是做了兩個button一個是有陰影的,另一個是扁平的,這裡隻貼出了扁平的按鈕
public class FlatButton extends JButton implements MouseListener{
protected String text;
protected Icon icon;
protected Color normalColor;
protected Color hoverColor;
protected Color pressedColor;
protected Color normalTextColor;
protected Color hoverTextColor;
protected Color pressedTextColor;
protected boolean selected;
private boolean hover = false;
public FlatButton(String text, Icon icon) {
this(text,icon, MaterialColors.GRAY_800,MaterialColors.GRAY_700,MaterialColors.GRAY_600,MaterialColors.WHITE,MaterialColors.WHITE,MaterialColors.BLUE_400);
}
public FlatButton(String text, Icon icon, Color normalColor, Color hoverColor, Color pressedColor, Color normalTextColor, Color hoverTextColor, Color pressedTextColor) {
super(text,icon);
this.text = text;
this.icon = icon;
this.normalColor = normalColor;
this.hoverColor = hoverColor;
this.pressedColor = pressedColor;
this.normalTextColor = normalTextColor;
this.hoverTextColor = hoverTextColor;
this.pressedTextColor = pressedTextColor;
init(text,icon);
}
@Override
protected void init(String text, Icon icon) {
super.init(text, icon);
this.setBorderPainted(false);
this.setFocusPainted(false);
this.setBackground(normalColor);
this.setForeground(normalTextColor);
this.addMouseListener(this);
}
public Color getNormalColor() {
return normalColor;
}
public void setNormalColor(Color normalColor) {
this.normalColor = normalColor;
this.setBackground(normalColor);
repaint();
}
public Color getHoverColor() {
return hoverColor;
}
public void setHoverColor(Color hoverColor) {
this.hoverColor = hoverColor;
repaint();
}
public Color getPressedColor() {
return pressedColor;
}
public void setPressedColor(Color pressedColor) {
this.pressedColor = pressedColor;
repaint();
}
public Color getNormalTextColor() {
return normalTextColor;
}
public void setNormalTextColor(Color normalTextColor) {
this.normalTextColor = normalTextColor;
repaint();
}
public Color getHoverTextColor() {
return hoverTextColor;
}
public void setHoverTextColor(Color hoverTextColor) {
this.hoverTextColor = hoverTextColor;
repaint();
}
public Color getPressedTextColor() {
return pressedTextColor;
}
public void setPressedTextColor(Color pressedTextColor) {
this.pressedTextColor = pressedTextColor;
repaint();
}
public Boolean getSelected() {
return selected;
}
@Override
public void setSelected(boolean b) {
super.setSelected(b);
this.selected = b;
if (b){
this.setBackground(pressedColor);
} else {
this.setBackground(normalColor);
}
this.setForeground(normalTextColor);
repaint();
}
@Override
public void mouseClicked(MouseEvent e) {
}
@Override
public void mousePressed(MouseEvent e) {
this.setForeground(this.pressedTextColor);
this.setBackground(this.pressedColor);
}
@Override
public void mouseReleased(MouseEvent e) {
if (hover){
this.setBackground(this.hoverColor);
this.setForeground(this.hoverTextColor);
} else {
this.setForeground(this.normalTextColor);
this.setBackground(this.normalColor);
}
}
@Override
public void mouseEntered(MouseEvent e) {
hover = true;
if (!selected){
this.setBackground(this.hoverColor);
this.setForeground(this.hoverTextColor);
}
}
@Override
public void mouseExited(MouseEvent e) {
hover = false;
if (!selected){
this.setBackground(this.normalColor);
this.setForeground(this.normalTextColor);
}
}
}
相當于一個比較基礎的按鈕,實作了滑鼠懸浮,點選變色等,可以繼承做出更好的效果
比較常用的元件
還有一些例如展示商品的card都可以提前做出來,等待調用
/**
*這是一個非常簡單的分割線元件
*/
public class Divider extends JLabel {
private Color color;
private int thickness;
public Divider(Color color, int thickness){
super();
this.color = color;
this.thickness = thickness;
// this.setBorder(BorderFactory.createLineBorder(color, thickness));
this.setBorder(new EmptyBorder(thickness,0,0,0));
}
public Divider(){
this(MaterialColors.GRAY_700,1);
}
public Divider(int thickness){
this(MaterialColors.GRAY_700, thickness);
}
public Divider(Color color){
this(color,1);
}
@Override
public void paint(Graphics g) {
Graphics2D graphics2D = (Graphics2D) g.create();
graphics2D.setColor(color);
graphics2D.fillRect(0,0,getWidth(),thickness);
graphics2D.dispose();
}
}
動畫和繪畫
動畫可以根據Timer定時器來制作,具體可以參見上面的輸入框裡的寫法,動畫效果隻要入門就比較簡單,主要是坐标的計算問題
繪畫也是坐标的計算
//這是一個繼承了FlatButton的元件重寫的繪制方法
@Override
public void paint(Graphics g) {
super.paint(g);
if (selected){
Graphics2D graphics2D = (Graphics2D) g.create();
graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
GradientPaint paint = new GradientPaint(0, 0, pressedTextColor,0,0,pressedTextColor);
graphics2D.setPaint(paint);
graphics2D.fillRect(0,0,5,getHeight());
GradientPaint paint1 = new GradientPaint(0, 0, normalColor,0,0,normalColor);
graphics2D.setPaint(paint1);
graphics2D.drawRoundRect(2,getHeight() / 4,1,1,1,1);
graphics2D.drawRoundRect(2,getHeight() / 2,1,1,1,1);
graphics2D.drawRoundRect(2,getHeight() * 3 / 4,1,1,1,1);
graphics2D.dispose();
}
}

上面的代碼可以實作這樣的按鈕效果
界面建構
簡單界面布局
布局 | 簡介 | 優點 | 缺點 | 坑點 |
---|---|---|---|---|
FlowLayout | 行布局在一行填滿之後換到下一行 | 簡單易用 | 不夠靈活,隻能成行排列,不能以其他方式排列布局,標明方向後所有元件依次排列,不能更改某一個的位置。 | 在嵌套JScrollPanel時不會自動換行排列 |
BorderLayout | 邊界布局,可以按照五個方向排列元件 | 可以在五個方向排列元件,已經能應付絕大多數簡單的布局 | 隻有五個方向,不足以應付所有的布局,其中的元件預設是充滿父元件,盡量的大,尤其是Center元件的填充,容易使布局變醜 | 如果在add時候不指定BorderLayout左右的元件都會添加到中間,後添加的元件會覆寫之前添加的 |
GridLayout | 表格布局,實作多行多列的排列 | 在大小相等的多行排列中變現出色 | 不能調整每一行每一列的寬度,必須等寬,行高也登高,而且會為了填充父布局,拉伸子元件 | 在元件數量較少的時候會填充整個父元件導緻子元件很大很大 |
CardLayout | 在一個Panel裡同時放多個Panel | 便于切換整體布局,比如導航欄對應的部分 | 如果存放的幾個JPanel的大小不一緻可能會有問題 | 儲存JPanel的時候如果給它一個名字,點用的時候不要記錯 |
複雜界面布局(GridBagLayout)
可以說在我寫項目的時候幾乎所有的難以用其他布局完成的布局GridBagLayout都可以輕松完成,但是這個GridBagLayout也比較繁瑣,用法簡單但是代碼比較多
//需要有配套的Constrant
GridBagLayout gridBagLayout = new GridBagLayout();
GridBagConstraints gridBagConstraints;
JPanel contentPanel = new JPanel(gridBagLayout);
gridBagConstraints = new GridBagConstraints();
//第幾行
gridBagConstraints.gridx = 1;
//第幾列
gridBagConstraints.gridy = 1;
//在這一個各自中的位置
gridBagConstraints.anchor = GridBagConstraints.EAST;
//占據父容器的比重
gridBagConstraints.weightx = 0.7;
//占據子容器的比重
gridBagConstraints.weighty = 0.7;
//填充方向,水準,豎直或者所有
gridBagConstraints.fill = GridBagConstraints.BOTH;
//還有gridWidth是占據幾行,gridHeight是幾列
//然後inset是邊距,不推薦使用,但是效果也還可以
contentPanel.add(new JLabel("test"), gridBagConstraints);
這是利用GridBagLayout實作的一個界面
每一個元件需要一個GridBagConstraints來限制他的表現,可以限制的大概有以下幾點
- 元件占據的比重(weightx/weighty)
- 元件位于的表格(gridx/gridy)
- 組建的空白(insert)
- 元件占據幾行幾列,不完全規則的網格布局(gridheight/gridwidth)
- 元件的對齊方式,支援9個方位,基本完全足夠(anchor)
有幾點需要注意
- insert不建議使用,會導緻父元件背景色顯示出現問題
- anchor的固定對齊是相對于某個網格的對齊,不是針對整個頁面
- anchor如果确實想要利用GridBagLayout将元件放置在比如右下角,在隻有一個元件的情況下需要規定weight和weighty都是1.0或者更大
- 網格布局的最終依舊保證每一行等高,每一列等寬
連接配接伺服器
Java swing是線程不安全的,所有的更新UI都要在UI線程中完成,或者利用SwingUtil.invokeLater()進行更新。
在連接配接網絡的時候,網絡的延時比較大,需要通過新開線程的方式進行連接配接。在UI線程中連接配接網絡,會造成UI線程的卡頓,在網絡條件很差的情況下,會卡死。
要善于利用線程池,防止同時連接配接數量過大,伺服器過載(畢竟是自家小水管)
servlet連接配接
如果後端用servlet或者是其他方式傳遞資料,傳回Json格式,或者是個人規定好的格式的資料。記住一下幾點:
- 無論後端給了多好(垃圾)的接口一定要二次封裝,便于以後功能的填充,或者是在明知道後端應該祭天的情況下給自己留一條活路
- 二次封裝的方法不應是異步方法,或者處理Future資料也可以
- 一開始的時候要列印出傳回資料,便于調試
//某一個封裝樣例,ResponseEntity是根據後端傳回的資料寫的封裝類
//ConnectControl是連接配接網絡的方法處理後端傳回的資料
public class CommentControl {
CommentControl(){}
public ResponseEntity addComment(String token, int lastCommentId, int goodId, String message){
ConnectControl connectControl = new ConnectControl();
Map<String, String> map = new HashMap<>();
map.put("lastCommentId", lastCommentId + "");
map.put("goodId", goodId + "");
map.put("message", message);
connectControl.doPost(BaseValue.load + "addComment", token, map);
return ResponseEntity.fit(connectControl);
}
public ResponseEntity getCommentByGoodId(int goodId){
ConnectControl connectControl = new ConnectControl();
Map<String, String> map = new HashMap<>();
map.put("goodId", goodId + "");
connectControl.doPost(BaseValue.load + "getCommentByGoodId", null, map);
return ResponseEntity.fit(connectControl);
}
}
socket
如果要做網絡請求,尤其是類似于C/S模型的聊天socket是必不可少的。實作方法一般是while(true)嵌套socket進行連接配接
- 可以在while(true)增加Thread.sleep(100)來緩沖,防止請求過于頻繁
- 可以用到上面講的java.awt的Timer定時器來實作定時的效果
public class ChatControl {
private final static int RETRY_TIME = 5;
private static int registerCount = 0;
private static int sendCount = 0;
private static int receiveCount = 0;
private static int userId;
private static Executor fixedThreadPool = Executors.newFixedThreadPool(3);
private static Socket socket;
private static DataInputStream inputStream;
private static DataOutputStream outputStream;
private static final List<String> messages = new ArrayList<>();
private static TrayIcon trayIcon;
static {
try {
socket = new Socket(BaseValue.host,BaseValue.chatPort);
inputStream = new DataInputStream(socket.getInputStream());
outputStream = new DataOutputStream(socket.getOutputStream());
} catch (IOException e) {
e.printStackTrace();
System.out.println("socket啟動失敗");
}
}
ChatControl(){
//這一部分還挺有意思的,是調用系統消息提示,增加托盤圖示
SystemTray tray = SystemTray.getSystemTray();
trayIcon = new TrayIcon(Icons.shop.getImage(), "閑置物品交易平台");
//Let the system resize the image if needed
trayIcon.setImageAutoSize(true);
//添加托盤圖示
trayIcon.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
JFrame f = FrameBase.frameBaseList.get(0);
f.setVisible(true);
f.setExtendedState(JFrame.NORMAL);
}
});
try {
tray.add(trayIcon);
} catch (AWTException e) {
e.printStackTrace();
}
receive()
}
public void send(String message) {
ResponseEntity responseEntity = new ResponseEntity(0, "",message);
String json = JSONObject.toJSONString(responseEntity);
try {
outputStream.writeUTF(json);
outputStream.flush();
} catch (IOException e) {
if (sendCount == 0){
e.printStackTrace();
}
if (sendCount <= RETRY_TIME){
System.out.println("sending" + sendCount);
sendCount++;
send(message);
} else {
System.out.println("嘗試發送失敗");
sendCount = 0;
}
}
}
public void receive() {
fixedThreadPool.execute(new Runnable() {
@Override
public void run() {
while (true){
try{
String result = inputStream.readUTF();
JSONObject object = JSONObject.parseObject(result);
if (object.getInteger("code") == 0){
Stringmessage = JSONObject.parseObject(object.getString("data"), String.class);
trayIcon.displayMessage("新消息", message.getText(), TrayIcon.MessageType.NONE);
Thread.sleep(200);
messages.add(message);
}
} catch (IOException | InterruptedException e){
if (receiveCount == 0){
e.printStackTrace();
}
if (receiveCount <= RETRY_TIME){
System.out.println("retry" + receiveCount);
receiveCount++;
if (receiveCount > RETRY_TIME){
System.out.println("socket連接配接丢失");
receiveCount = 0;
return;
}
receive();
}
}
}
}
});
}
//外部隻需調用getMessages就能擷取消息
public List<Message> getMessages(){
if (messages.size() == 0){
return new ArrayList<>();
} else {
List<String> messageList = new ArrayList<>(messages);
messages.clear();
return messageList;
}
}
public void closeSocket() {
try {
socket.close();
outputStream.close();
inputStream.close();
} catch (IOException e){
e.printStackTrace();
}
}
}
上面的代碼外部調用隻需要執行個體化之後調用getMesages方法就可以擷取資訊,不用持有外部引用,會降低記憶體洩漏的風險
外部隻要再用Timer定時查詢資料更新ui就可以了
項目連結
Swing C/S交易市場源碼