一直以來用代碼來寫圖形界面是我從來沒有做過的事,(-。-;)額,但是已經選擇軟開這條路,我覺得什麼都是要會一點,這樣的話也許大概可能多個月後重新寫東西能夠得心應手很多吧。而且,以後自己要是忘記了,也可以在這裡看看,順便提高高自己文學能力。原諒我敲字比較難看懂,這些當中多多少少是存在自己情感寫出來的,看正文好了。
Read after me:本文是適合一些剛入門學習圖形化界面的博友,當然要多多少少了解java之類的基礎知識(類相關知識,泛型集合等),否則看看好了。工程文件將會在最後給出附錄。我比較討厭書本上一行一行羞澀難懂的文字,一點鮮活性都沒有,大概和我不喜歡背書有關系o(^▽^)o。
此次我將以一個游戲為例開始介紹我自己感悟出來的圖形化界面的一些知識,其實我也只是個菜鳥。
游戲:貪吃蛇;
開發工具:Eclipse Java Naon,jdk1.7以上;
開發環境:Windows10或Ubuntu14.04(我在這兩個平台下是編譯過,效果的話,我是推薦Ubuntu,因為不知道為什麼Windows下界面經常為空的?要點擊run as Java application好多好多下,(-。-;)也不見得會有游戲界面出現。 如果有博友知道,希望能夠指點下小弟,在此先謝謝)。
游戲界面在此(嗯,比較簡素)

——————————————————我是分割線————————————————————
既然是寫圖像界面,那麼先要有個框架是伐!那麼JFrame就是這個圖像界面框架類的祖先,所以說我們要繼承這個類對吧。先新建一個class,就是new>>class>>XXXX.java;我是建議不同的class放在不同的包(Package)中,這樣條例清楚(這是句廢話)。我先起名這個Frame的名字叫做SnakeFram.java,代碼見下,看注釋(敲黑板);

1 package shu.hcc.ui;
2
3 import javax.swing.JFrame;
4 import java.awt.Toolkit;
5 import java.awt.Dimension;
6
7
8 public class SnakeFrame extends JFrame {
9
10 //每個Frame都有個id
11 private static final long serialVersionUID = 1L;
12 //Frame窗口大小
13 private final int _windowWidth = 530;
14 private final int _windowHeight = 450;
15
16 public SnakeFrame()
17 {
18
19 this.setTitle("貪吃蛇初稿1.0");
20 this.setSize(_windowWidth, _windowHeight);
21
22 Toolkit _toolKit = Toolkit.getDefaultToolkit();//獲取電腦屏幕大小
23 Dimension _screenSize = _toolKit.getScreenSize();
24 final int _screenWidth = _screenSize.width;
25 final int _screenHeight = _screenSize.height;
26
27 this.setLocation((_screenWidth-this.getWidth())/2,(_screenHeight-this.getHeight())/2);//注意計算後居中
28
29 this.setDefaultCloseOperation(EXIT_ON_CLOSE); //注意默認無關閉操作,即後台不死
30
31 this.setResizable(false);//設為窗口不變,注意默認可拉伸啥的
32
33 this.setVisible(true);//設為可見,注意默認不可見
34
35 this.setLayout(null);//setlayout有很多中布局方式,我這暫時設為NULL,後面會做個layout,將其插入,
36 }
37
38 }
SnakeFrame.java
這個比較簡單,一般的話JFrame這樣寫就ok了,我也就不細講了,看注釋恩。
在01JFrame代碼裡有提到過,它是含有Layout = NULL,所以我們利用JPanel給它提供一個Layout,同時這個Layout使能夠刷新的,說白一點,游戲一直在顯示新圖就是刷新界面嘛!!!
這樣我們繼續新建第二個class,命名為SnakePaint.java,它是繼承extends JPanel的,最後在
Frame.setContentPanel(panel);
呢就能將該panel作為layout貼到Frame中去。
首先我們需要一個控制按鈕畫板對吧,還有顯示我們吃了多少食物的得分的label(我是利用button來實現的,因為自帶的label我不會用(尴尬)),見代碼:

1 private void _initButton()
2 {
3 this.setLayout(null);
4 _showLabel= new JLabel();
5 _showLabel.setFont(new java.awt.Font("Dialog",1,20));//字形,粗細,大小
6 _showLabel.setText("得分:");
7 _showLabel.setForeground(Color.BLUE);
8 _showLabel.setLocation(420, 100);
9 _showLabel.setBackground(new Color(240,240,240));
10 _showLabel.setSize(80,60);
11 this.add(_showLabel);
12
13 _showScore= new JButton("0");
14 _showScore.setSize(50,60);
15 _showScore.setLocation(480, 100);
16 _showScore.setBackground(new Color(240,240,240));
17 _showScore.setFont(new java.awt.Font("Dialog",1,20));
18 _showScore.setBorder(null);
19 _showScore.setEnabled(false);
20 this.add(_showScore);
21
22 _startButton = new JButton();
23 _startButton.setText("開始游戲");
24 _startButton.setLocation(420, 250);
25 _startButton.setSize(90, 30);
26 this.add(_startButton);
27 //_startButton.addActionListener(new _StartActionListener());
28
29 _aboutButton = new JButton();
30 _aboutButton.setText("關於游戲");
31 _aboutButton.setLocation(420,300);
32 _aboutButton.setSize(90, 30);
33 this.add(_aboutButton);
34 //_aboutButton.addActionListener(new _AboutActionListener());
35 this.requestFocus();
36 }
initButton()
接著是繪制我們的游戲畫板,用的是@Override private void paintComponent()方法,這樣的話在游戲進程中只要調用panel.repaint()函數,就能執行paintComponent()函數。所以說呢我們刷新游戲界面只要更新paintComponent()函數的畫圖數據就ok。

1 @Override
2 public void paintComponent(Graphics pen)//super.paintComponent(g)是父類JPanel裡的方法,會把整個面板用背景色重畫一遍,起到清屏的作用
3 {
4
5 try{
6 super.paintComponents(pen);
7 _CreatGameInit(pen);
8 if(_isGG)
9 {//游戲是否結束?
10 _GameOverDisplay(pen);
11 }
12 if(_isStart)
13 { //游戲是否開始?
14 _CreateSnake(pen);
15 _CreateFood(pen);
16 //倒計時
17 if(_isCount > -1)
18 {_CreateTip(pen);}
19 }
20 }
21 catch(Exception e)
22 {
23 System.out.println("ERROR");
24 }
25 this.requestFocus();
26
27 }
paintComponent()
上述代碼裡,第一,super.paintComponents(pen);是用來更新控件的,不能少,否則控制面板就沒咯。第二,我把畫網格和畫蛇食物的函數分開寫了,我是根據畫網格坐標來定(原點在左上角(敲黑板))的見下:

1 private void _CreatGameInit(Graphics pen)
2 {
3 pen.setColor(Color.BLACK);
4 pen.drawRect(_x, _y, _panelWidth, _panelHeight);
5
6 pen.setColor(Color.WHITE);
7 pen.fillRect(_x+1, _y+1, _panelWidth-1, _panelHeight-1);
8
9 pen.setColor(Color.GRAY);
10 for(int i=1;i<this._panelWidth/this._tileSize;++i)
11 {
12 pen.drawLine(this._x+i*_tileSize, this._y, this._x+i*_tileSize, this._y+this._panelHeight);
13 }
14 for(int i=1;i<this._panelHeight/this._tileSize;++i)
15 {
16 pen.drawLine(this._x, this._y+i*this._tileSize, this._x+this._panelWidth, this._y+i*this._tileSize);
17 }
18 }
更新網格的

1 private void _CreateSnake(Graphics pen)
2 {
3 _snakeList = _snake._GetSnakeList();
4 if(_snakeList == null)
5 {
6 return;
7 }
8
9 for(int i=0;i<_snakeList.size();++i)
10 {
11 if(!_snakeList.get(i)._CrashLine()){
12 if(i == _snakeList.size()-1){
13 _SnakePaint(_bufferImageSnakeHead,pen,this._x+(_snakeList.get(i)._x)*_tileSize,this._y+(_snakeList.get(i)._y)*_tileSize,this._x+(_snakeList.get(i)._x+1)*_tileSize,this._y+(_snakeList.get(i)._y+1)*_tileSize,_snake._GetNextDirection());
14 }
15 else if(i == 0){
16 _SnakePaint(_bufferImageSnakeTrail,pen,this._x+(_snakeList.get(i)._x)*_tileSize,this._y+(_snakeList.get(i)._y)*_tileSize,this._x+(_snakeList.get(i)._x+1)*_tileSize,this._y+(_snakeList.get(i)._y+1)*_tileSize,TailDirection(_snakeList.get(0),_snakeList.get(1)));
17 // pen.fillRect(this._x+_snakeList.get(i)._x*_tileSize ,this._y+_snakeList.get(i)._y*_tileSize ,Snake.SNAKE_WIDTH/2 , Snake.SNAKE_HEIGHT/2);
18 }
19 else{
20 pen.drawImage(_bufferImageSnakeBody,this._x+(_snakeList.get(i)._x)*_tileSize ,this._y+(_snakeList.get(i)._y)*_tileSize , this._x+(_snakeList.get(i)._x+1)*_tileSize ,this._y+(_snakeList.get(i)._y+1)*_tileSize,10,10, 20,20,this);
21 }
22 }
23 }
24 }
25 final Dimension step[]={new Dimension(0,-1),new Dimension(0,1),new Dimension(-1,0),new Dimension(1,0)};
26 private void _SnakePaint(BufferedImage image,Graphics pen,int dx1,int dy1,int dx2,int dy2,int dir)
27 {
28 pen.drawImage(image,dx1,dy1 ,dx2 ,dy2,coo(dir,10).width,coo(dir,10).height, coo(dir,20).width,coo(dir,20).height,this);
29 }
更新蛇

1 private void _CreateFood(Graphics pen)
2 {
3 _foodList = _food._GetFoodList();
4 if(_foodList == null)
5 {
6 return;
7 }
8 pen.setColor(Color.BLACK);
9 for(int i=0;i<_foodList.size();++i)
10 {
11 pen.drawImage(_bufferImageStrawberry,this._x+(_foodList.get(i)._x-1)*_tileSize, this._y+(_foodList.get(i)._y-1)*_tileSize,this);
12 }
13 }
更新食物

1 private void _CreateTip(Graphics pen)
2 {
3 pen.setColor(Color.BLUE);
4 String show;
5 if(_isCount > 0)
6 {//游戲開始
7 //顯示計數
8 pen.setFont(new Font("Dialog", Font.BOLD, 100));
9 show = new String(String.valueOf(_isCount));
10 }
11 else
12 {
13 //顯示開始
14 pen.setFont(new Font("Dialog", Font.BOLD, 64));
15 show = new String("開始");
16 }
17 pen.drawString(show,150,225);
18 }
19
20 private void _GameOverDisplay (Graphics pen)
21 {
22 Font font = new Font("宋體", Font.BOLD, 64);
23 pen.setFont(font);
24 pen.setColor(Color.RED);
25 pen.drawString("游戲結束",60,100);
26 }
更新游戲開始提示和結束提示
最後,我們有圖了,沒有感覺缺少點什麼麼?對,就是控制,你怎麼控制游戲來更新畫板panel???這裡,我們安裝個玩家控制器,說白點就是當有按鍵敲下時,我們panel能夠響應按下哪個鍵(即執行函數對吧)!!!

1 public void setGameControl(SnakeControl control){
2 this.addKeyListener(control);
3 }
panel響應鍵盤
SnakeControl是一個第三個類,Implements 於KeyListener類。好,接下講!!!
在02中提到過,我們建起第三個類,稱為SnakeControl.java ,它是Implements 於KeyListener類,這時候它就可以通過重寫@Override一些響應鍵盤的函數,比如keyPressed(KeyEvent e) 按鍵按下的函數。

1 @Override
2 public void keyPressed(KeyEvent e) {
3 // TODO Auto-generated method stub
4 //System.out.println(e.getKeyCode());
5 switch(e.getKeyCode())
6 {
7 case KeyEvent.VK_UP:
8 if(_nextDirection !=Snake.Dir.DOWN)
9 {
10 _nextDirection = Snake.Dir.UP;
11 }break;
12 case KeyEvent.VK_DOWN:
13 if(_nextDirection != Snake.Dir.UP)
14 {
15 _nextDirection = Snake.Dir.DOWN;
16 }break;
17 case KeyEvent.VK_LEFT:
18 if(_nextDirection != Snake.Dir.RIGHT)
19 {
20 _nextDirection = Snake.Dir.LEFT;
21 }break;
22 case KeyEvent.VK_RIGHT:
23 if(_nextDirection != Snake.Dir.LEFT)
24 {
25 _nextDirection = Snake.Dir.RIGHT;
26 }break;
27
28 }
29 _snake._SetNextDirection(_nextDirection);
30
31 }
按鍵響應函數
當按下一個方向鍵(我是這樣設的,當然可以自己設置),同時我做了個防吃到自己的,什麼意思?就是比如當蛇向上走,那麼按下按鍵肯定不能向下,要不就吃到自己了,所以蛇有三個方向可以選擇_nextDirection = Snake.Dir.UP;或_nextDirection = Snake.Dir.Left;_nextDirection = Snake.Dir.Right;這就要看按鍵KeyEvent的值了。這裡KeyEvent的鍵值大家可以搜下有那些。Snake的類函數我在之後會給出,本來寫這樣的游戲代碼要先給出一些規定的蛇Snake,食物Food,坐標Coordination的類成員結構,但是我這主要是提圖形界面如何寫,所以就放在後面(廢話)。我們在有按鍵按下的時候為什麼不直接就寫個函數來改變方向呢?只是暫時將值存在下一個方向_nextDirection裡呢?這是因為我們刷新界面是有另一個線程Runnable來做的,它會定時刷新界面,只要知道我蛇的下一個方向就ok,這樣的話省下一大堆麻煩(可能難理解,我也不知道該怎麼表述,要自己體會的)。
接上講,我們說過我們要刷新界面,就是要有另外一個獨立於主進程之外的線程來實現,我用的是Runnable,其結構如下:
private Runnable tdpaint = new Runnable()
{
public void run()
{
//TODO:當開啟此線程將執行該函數
}
};
我們只要在控制按鍵StartButton中添加上
private Thread _tdpaint = new Thread(tdpaint);//新建個線程 _tdpaint.start();//線程跑起來
便可以跑起我們的游戲進程線程。
還有另外一種方式來寫線程,就是extends Thread

1 private abstract class SnakeTd extends Thread
2 {
3 private boolean suspend = false;
4 private String control = "";
5 //暫停
6 public void SetSuspend(boolean suspend){
7 if(!suspend){
8 synchronized (control){
9 control.notifyAll();
10 }
11 }
12 this.suspend = suspend;
13 }
14 //游戲running
15 public void run()
16 {
17 while(!_snakeControl._IsEndGame())
18 {
19 synchronized (control){
20 if(suspend){
21 try {
22 control.wait();
23 }catch(Exception e){
24 e.printStackTrace();
25 }
26 }
27 _snakeControl._GameRunning();
28 }
29
30 }
31 _startButton.setText("再來一局");
32 }
33 }
34
35 }
36
Snake Runnig
我們只要在游戲進程Runnable中添加上
private SnakeTd _GameTd;
_GameTd =new SnakeTd(){};
_GameTd.start();//跑起來
便能跑起蛇運動的線程。
之所以寫這兩個線程方式,一個方面是自己想要練習如何編程線程,另外,游戲不僅僅只有蛇運動,還有開場倒計時(雖然我沒做很炫的倒計時)之類的,所以我做了兩個進程,第一個Runnable是游戲的主進程,當開場倒計時後,會調用Thread線程來run snake。還有是如果要暫時暫停線程,我只會第二種方式,為什麼要暫停進程,是這樣的,我做了個關於button的功能,如果我點擊button,會彈出程序的相關信息,但是不會暫停游戲,這就尴尬了,所以我特意學習了如何暫停進程這種功能。
接上講,我提到過,我們在一個新線程thread來跑游戲GameRunning,那麼游戲數據要更新後就應該repaint界面了,

1 public void _GameRunning()
2 {
3 //開始游戲蛇開始跑動
4 _Move(_nextDirection);
5 try {
6 Thread.sleep(200); //暫停200
7 } catch (InterruptedException e) {
8 e.printStackTrace();
9 }
10 }
GameRunning

1 private void _Move(int dir)
2 {
3 if(!_IsEndGame())//游戲結束判斷?
4 {
5 this._snakeList.add(new Coordination(this._snakeList.get(_snakeList.size()-1)._x+step[dir].width,this._snakeList.get(_snakeList.size()-1)._y+step[dir].height));
6 if(_IsEat())//是否吃到食物判斷
7 {
8 _AfterEat();//吃到食物該怎麼辦?
9 }
10 else{
11 _snakeList.remove(0);//沒有吃到食物又該怎麼辦?
12 }
13 this._panel.repaint();//游戲界面刷新
14 }
15 }
_Move

public boolean _IsEndGame()
{
if( _IsCrashBody() || _IsCrashWall())
{
_AfterCrashWall();
return true;//如果撞到牆或吃到自己就會執行AfterCrashWall,並返回true
}
return false ;//否則false,並沒有撞牆
}
游戲結束判斷

//判斷是調用了編寫在Coordination類中的函數來判斷,我接下去會講
private boolean _IsCrashWall()
{
return this._snakeList.get(_snakeList.size()-1)._CrashLine();
}
private boolean _IsCrashBody()
{
return this._snakeList.get(_snakeList.size()-1)._CrashBool(this._snakeList.get(i));
}
碰撞返回

private void _AfterCrashWall()
{
//碰撞後就應該將數據什麼的歸0對法
_panel._SetStart(false);
_panel._SetGG(true);
_snakeList.remove((_snakeList.size()-1));
_panel.repaint();
}
碰撞後

private boolean _IsEat()
{
//吃到食物,也是利用編寫在Coordination中get()函數來判斷蛇頭與食物的坐標
return this._snakeList.get(this._snakeList.size()-1)._CrashBool(this._foodList.get(this._foodList.size()-1));
}
判斷是否吃到食物

private void _AfterEat()
{
this._food._InitFoodLocation(this._snake);//食物被吃到了,就應該更新下一個食物點
jb.setText(String.valueOf( _snake._GetSnakeList().size() - 5));//吃到食物,成績+1
this._foodList.remove(0);//要被這個食物去掉
}
吃到食物後
游戲跑起來,都是在判斷。我這裡說下,首先不管如何,下一幀蛇必須要在_snakeList(類型為ArrayList或Vector之類的容器)尾部add上新一點,然後如果吃到食物,就不需要將頭部remove(0),否則remove(0)。那麼如果判斷吃到食物、或者撞到牆了呢?我是利用下一個類Coordination,來判斷坐標的的。
如果游戲結束了的話,需要將數據進行初始化,然後點擊開始下一局就可以重寫開始。
蛇或食物之類的有大小,還應該都是有一個結構體的來存放它的坐標點,另外蛇有它下一步的方向。這個結構體我是用Vector容器來裝的見下:
Vector<Coordination> _snakeList = new Vector<Coordination>();
Coordiantion.java是一個類,結構如下:

1 package shu.hcc.snake;
2
3 public class Coordination {
4 /*
5 * 蛇或食物的坐標類
6 */
7 public int _x;
8 public int _y;
9
10 public Coordination(int x,int y)
11 {
12 this._x = x;
13 this._y = y;
14 }
15
16 //碰撞判斷--方法
17 public boolean _CrashBool(Coordination other)
18 {
19 return (this._x == other._x && this._y == other._y)?true:false
20 }
21 //出界碰撞判斷(碰牆)--方法
22 public boolean _CrashLine()
23 {
24 return (this._x<0)||(this._x>39)||(this._y<0)||(this._y>39)? true:false;
25 }
26 //尾巴方向判斷
27 public int _TailDirection(Coordination other)
28 {
29 if(this._x-other._x ==0 && this._y -other._y <0)
30 return 0;
31 else if(this._x-other._x ==0 && this._y -other._y >0)
32 return 1;
33 else if(this._x-other._x < 0 && this._y -other._y ==0)
34 return 2;
35 else if(this._x-other._x > 0 && this._y -other._y == 0)
36 return 3;
37 return 0;
38 }
39 }
Coordination
其中_x,_y分別是坐標的橫縱坐標軸值,_snakeList每次add,都將下一點的橫縱坐標存入,同時每個_snakeList.get(i)都是Coordination的對象,所以可以調用_crashLine(),_crashBool()函數來判斷是否碰撞。
Snake.java
初始化時,我們隨機給定坐標:

public Snake()
{
_InitSnake();
}
public void _InitSnake()
{
Random _random = new Random();
_nextDirection = _random.nextInt(3);
Dimension step[]={new Dimension(0,-1),new Dimension(0,1),new Dimension(-1,0),new Dimension(1,0)};
Dimension aa;
do{
aa = new Dimension(_random.nextInt(40),_random.nextInt(40));
}while(aa.height<8 ||aa.height >32 || aa.width<8 || aa.width >32);
for (int i= 0;i<5; ++i)
{
_snakeList.add(new Coordination(aa.width+step[_nextDirection].width * i,aa.height +step[_nextDirection].height * i));
}
}
Snake()
Food.java
初始化,有個判斷要保證隨機生成的食物坐標不能和蛇的坐標重復,否則的話,這就會有bug。沒重復的坐標的話,Flag_For=true,此時就不需要在重新生成食物坐標,Flag_while=false,結束掉該生成循環。

public Food(Snake snake)
{
_foodList = new Vector<Coordination>() ;
_InitFoodLocation(snake);
}
public void _InitFoodLocation(Snake snake)
{
_snakeList = snake._GetSnakeList();
boolean Flag_While = true;
boolean Flag_For = true;
Random _random = new Random();
while(Flag_While){
Flag_For = true;
_foodList.add(new Coordination(_random.nextInt(40),_random.nextInt(40)));
for(int i=0;i<_snakeList.size();++i)
{
if(_foodList.get(0)._CrashBool(_snakeList.get(i)))
{
Flag_For = false;
_foodList.remove(0);
break;
}
}
if(Flag_For)
Flag_While = false;
}
}
Food()
該寫的基本上寫完了,限於本人文學寫作能力,有些可能不夠詳細,各位觀眾博友請看工程文件好了好了。
如報錯的話,請修改加載圖片的路徑,我改了好久也沒把相對路徑搞出來,蛋疼。。。
項目是在ubuntu下做的,如果在windows下,可能需要修改下其他的地方。。。
以下是游戲截圖:

總結:其實圖形界面編程不是很復雜,恩,JFrame,JPanel類,paintComponent(),repaint()函數,還有響應KeyListener()鍵盤函數,線程Thread(),Runnable(),泛型容器Vector<>,ArrayList<>。
核心部分,都是計算的方法,我不敢稱為算法,因為我覺得太low點,以後我還會貼上一些算法。
圖形界面編程比較直觀能夠很好的顯現出具體的結果,但是沒有算法核心,只是一團low的小游戲。
<注:未完待續>