Java畫圖程序設計

本文講述一個畫圖板應用程序的設計,屏幕抓圖以下:
程序截圖java

『IShape』數組

這是全部圖形類(此後稱做模型類)都應該實現接口,外部的控制類,好比畫圖板類就經過這個接口跟模型類「交流」。名字開頭的I表示它是一個接口(Interface),這是eclipse用的一個命名法則,以爲挺有用的,就借鑑來了。這個接口定義了兩個方法:eclipse

 

  1. public void draw(java.awt.Graphics2D g);  
  2. //每一個實現IShape的類都在這個方法裏面指定它的圖形顯示代碼。  
  3.   
  4. public void processCursorEvent(java.awt.event.MouseEvent evt, int type);  
  5. /* 
  6. 這個方法是在圖形(被用戶)繪製過程當中,發生相關的鼠標點擊和移動事件時調用的。 
  7. 第一個參數就是所發生的鼠標事件對象; 
  8. 第二個參數取值於IShape所定義的三個常數:RIGHT_PRESSED, LEFT_RELEASED,和CURSOR_DRAGGED。 
  9. */  

 

下面這個class diagram顯示了全部圖形類的結構圖。FreeShape, RectBoundedShape,和PolyGon這三個類直接實現了IShape接口。其中,FreeShape和RectBoundedShape是抽象類,分別表明不規則圖形(好比鉛筆畫圖)和以一個長方形爲邊界的規則圖形,因爲分屬於這兩個類別的圖形對於鼠標事件的處理基本上都是一致的,因此就抽象出來這兩個父類,避免重複代碼。PolyGon是一個具體類,它的命名沒有采用Polygon是爲了不同java.awt.Polygon重名。它表明的圖形是多邊形,因爲它獨特的鼠標處理方式,它不屬於上面兩種類型圖形的任何一種,因此它直接實現了IShape接口。ide

 圖

IShape接口所定義的兩個方法究竟是怎麼被用到的呢?這個問題如今還不能馬上解答。在下面的部分,咱們先講述FreeShape所定義的不規則圖形及其兩個具體子類PolyLine和Eraser,而後在這個基礎上講述一個縮略版的畫圖板類,到那個時候,上面問題的答案也就天然揭曉了。以後,咱們再繼續講述其餘的圖形類。函數

『FreeShape』工具

講到FreeShape,咱們不得不先說一下PointsSet這個類。這是一個util類,被FreeShape和PolyGon用到,表明一個有序的點集合,並提供方便的方法來加入新的點和讀取點座標。爲了方便對模型類代碼的理解,這裏列出PointsSet類的API。this

 

  1. public PointsSet();  
  2. 用默認的初始容量(10)建立一個對象。  
  3.   
  4. public PointsSet(int initCap);  
  5. 用指定的初始容量(initCap)建立一個對象。  
  6.   
  7. public void addPoint(int x, int y);  
  8. 加入一個新的點到這個集合的末端;若是舊的末端點跟新的點重合,則不重複加入。  
  9.   
  10. public int[][] getPoints();  
  11. 將全部點以一個二維數組(int[2][n])返回。第一行是x座標,第二行是y座標。  
  12.   
  13. public int[][] getPoints(int x, int y);  
  14. 相似上一個方法,只是最後將參數指定的點加在末尾(不管是否跟集合末端的點重合);  
  15. 這個方法只被PolyGon用到。  

 

好了,來看下面代碼中FreeShape對IShape接口的實現。FreeShape有三個屬性變量:color, stroke,和pointsSet。權限設成protected固然是給子類用啦。color就是色彩了,stroke用來指定使用線條的粗細(固然,Stroke類的對象還能夠指定交接點形狀之類的屬性,不過這裏都使用其默認值了),pointsSet固然就是包含了全部控制點(這裏叫控制點彷佛不太恰當,由於其實沒法利用這些點來「控制」的,不過也想不到其餘恰當的名字,就這麼叫吧)集合。值得注意的是構造函數裏面包含了起始點的座標,這個點在函數裏面被加到了控制點集中。spa

這類圖形對鼠標事件的處理很簡單,它只對IShape.CURSOR_DRAGGED類型的事件感興趣,每當發生這類事件的時候,就把鼠標拖拽到的新的點加入到控制點集中。固然了,根據上面看到的PointsSet.addPoint(int,int)這個方法的「個性」,這個點是否真的被加入還要看它是否跟舊的末端點重合。.net

 

  1. import java.awt.*;  
  2. import java.awt.event.MouseEvent;  
  3.   
  4. public abstract class FreeShape implements IShape {  
  5.       
  6.     protected Color color;  
  7.     protected Stroke stroke;  
  8.     protected PointsSet pointsSet;  
  9.     
  10.     protected FreeShape(Color c, Stroke s, int x, int y) {  
  11.         pointsSet = new PointsSet(50);  
  12.         color = c;  
  13.         stroke = s;  
  14.         pointsSet.addPoint(x, y);  
  15.     }  
  16.       
  17.     public void processCursorEvent(MouseEvent e, int t) {  
  18.         if (t != IShape.CURSOR_DRAGGED)  
  19.             return;  
  20.         pointsSet.addPoint(e.getX(), e.getY());  
  21.     }  
  22.   
  23. }  

 

FreeShape類沒有實現IShape接口的draw(Graphics2D)方法,很明顯,這個方法是留給子類來完成的。PolyLine和Eraser繼承了FreeShape,分別表明鉛筆繪出的圖形和橡皮擦。其中PolyLine的構造函數結構跟其父類類似,直接調用父類的super方法來完成;相比之下,Eraser類就有點「叛逆」了,它的參數裏面用一個JComponent替換了Color。Eraser類是經過畫出跟畫圖板背景色彩一致的線條來掩蓋原有圖形而實現橡皮擦的效果的,但因爲畫圖板的背景色是能夠調的(見抓圖的Color Settings部分),直接給Eraser的構造函數一個色彩對象不太合適,因此乾脆將畫圖板本身(JComponent)傳了進來,這樣,每次Eraser設定圖形色彩時,都直接問畫圖板要它的背景色。來看一下PolyLine對draw(Graphics2D)方法的實現:設計

 

  1. public void draw(Graphics2D g) {  
  2.       g.setColor(color);  
  3.       g.setStroke(stroke);  
  4.       int[][] points = pointsSet.getPoints();  
  5.       int s = points[0].length;  
  6.       if (s == 1) {  
  7.           int x = points[0][0];  
  8.           int y = points[1][0];  
  9.           g.drawLine(x, y, x, y);  
  10.       } else {  
  11.           g.drawPolyline(points[0], points[1], s);  
  12.       }  
  13.   }  

 

這個方法裏面有一個if-else結構,因爲構造函數裏面已經將起始點加入控制點集中,因此pointsSet.getPoints()會至少返回一個點。利用Graphics.drawPolyline(int[],int[],int)畫圖時,若是隻有一個點,它是不會畫出來東西的,因此檢查一下點數,若是隻有一個,則改用Graphics.drawLine(int,int,int,int)將這個點畫出來。Eraser的draw(Graphics2D)方法跟上面基本上徹底同樣,只是傳給Graphics.setColor(Color)的參數是經過JComponent.getBackground()獲得的。

『TestBoard』

如今就來看一個精簡版的畫圖板類:TestBoard。下面的代碼,是經過代碼註釋進行解釋的。須要注意的是,TestBoard自己還不能直接運行,須要把它放到一個JFrame裏面才行。同時畫圖工具的切換也須要外部的控件來處理。不過這些都比較簡單了,就很少說了。

 

  1. import java.awt.*;  
  2. import java.awt.event.*;  
  3. import javax.swing.*;  
  4. import java.util.ArrayList;  
  5.   
  6. public class TestBoard extends JPanel   
  7.                          implements MouseListener, MouseMotionListener {  
  8.       
  9.     //定義一些常量  
  10.     public static final int TOOL_PENCIL = 1;  
  11.     public static final int TOOL_ERASER = 2;  
  12.     public static final Stroke STROKE = new BasicStroke(1.0f);  
  13.     public static final Stroke ERASER_STROKE = new BasicStroke(15.0f);  
  14.   
  15.     private ArrayList shapes;     //保存全部的圖形對象(IShape)  
  16.     private IShape currentShape;  //指向當前還未完成的圖形  
  17.     private int tool; //表明當前使用的畫圖工具(TOOL_PENCIL或TOOL_ERASER)  
  18.       
  19.     public TestBoard() {  
  20.         //進行一些初始化  
  21.         shapes = new ArrayList();  
  22.         tool = TOOL_PENCIL;  
  23.         currentShape = null;  
  24.           
  25.         //安裝鼠標監聽器  
  26.         addMouseListener(this);  
  27.         addMouseMotionListener(this);  
  28.     }  
  29.       
  30.     //外部的控制界面能夠經過這個方法切換畫圖工具  
  31.     public void setTool(int t) {  
  32.         tool = t;  
  33.     }  
  34.       
  35.     //override JPanel的方法。經過調用IShape.draw(Graphics2D)方法來顯示圖形  
  36.     protected void paintComponent(Graphics g) {  
  37.         super.paintComponent(g);  
  38.         int size = shapes.size();  
  39.         Graphics2D g2d = (Graphics2D) g;  
  40.         for (int i=0; i < size; i++) {  
  41.             ((IShape) shapes.get(i)).draw(g2d);  
  42.         }  
  43.     }  
  44.       
  45.     public void mousePressed(MouseEvent e) {  
  46.         /* 當左鍵點擊時,currentShape確定指向null。根據當前畫圖工具建立相應圖形對象, 
  47.            將currentShape指向它,並把這個對象加入到對象集合(shapes)中。另外,調用 
  48.            repaint()方法將畫圖板的畫面更新一下。 */  
  49.         if (e.getButton() == MouseEvent.BUTTON1) {  
  50.             switch (tool) {  
  51.             case TOOL_PENCIL:  
  52.                 currentShape = new PolyLine(getForeground(),   
  53.                                                STROKE, e.getX(), e.getY());  
  54.                 break;  
  55.             case TOOL_ERASER:  
  56.                 currentShape = new Eraser(this, ERASER_STROKE,   
  57.                                                e.getX(), e.getY());  
  58.                 break;  
  59.             }  
  60.             shapes.add(currentShape);  
  61.             repaint();  
  62.         /* 當右鍵點擊而且currentShape不指向null時,調用currentShape的 
  63.           processCursorEvent(MouseEvent,int)方法,類型參數是 
  64.       IShape.RIGHT_PRESSED。 repaint()*/  
  65.         } else if (e.getButton() == MouseEvent.BUTTON3 && currentShape != null) {  
  66.             currentShape.processCursorEvent(e, IShape.RIGHT_PRESSED);  
  67.             repaint();  
  68.         }  
  69.     }  
  70.       
  71.     public void mouseDragged(MouseEvent e) {  
  72.         /* 當鼠標拖拽而且currentShape不指向null時(這種狀況下,左鍵確定處於 
  73.           按下狀態),調用currentShape的processCursorEvent(MouseEvent,int)方法, 
  74.           類型參數是IShape.CURSOR_DRAGGED。 repaint()*/  
  75.         if (currentShape != null) {  
  76.             currentShape.processCursorEvent(e, IShape.CURSOR_DRAGGED);  
  77.             repaint();  
  78.         }  
  79.     }  
  80.       
  81.     public void mouseReleased(MouseEvent e) {  
  82.         /* 當左鍵被鬆開而且currentShape不指向null時(這個時候,currentShape 
  83.           確定不會指向null的,多檢查一次,保險),調用currentShape的 
  84.           processCursorEvent(MouseEvent,int)方法,類型參數是 
  85.           IShape.CURSOR_DRAGGED。 repaint()*/  
  86.         if (e.getButton() == MouseEvent.BUTTON1 && currentShape != null) {  
  87.             currentShape.processCursorEvent(e, IShape.LEFT_RELEASED);  
  88.             currentShape = null;  
  89.             repaint();  
  90.         }  
  91.     }  
  92.       
  93.     //對下面這些事件不感興趣  
  94.     public void mouseClicked(MouseEvent e) {}  
  95.     public void mouseEntered(MouseEvent e) {}  
  96.     public void mouseExited(MouseEvent e) {}  
  97.     public void mouseMoved(MouseEvent e) {}  
  98.       
  99. }  

 

至此,整個程序的流程就很清楚了,文章開頭部分的問題也被解開了。接下來,就繼續來看其餘的模型類。

『RectBoundedShape』

RectBoundedShape構造函數的結構跟FreeShape同樣,在色彩和線條的運用上也是同樣的,也只對鼠標拖拽事件感興趣。不過,它只有兩個控制點,起始點和結束點,因此,不須要用到PointsSet。原本,RectBoundedShape這個類是比FreeShape簡單的,在處理鼠標拖拽事件時只要將結束點設置到新拖拽到的點就能夠了。不過,這裏咱們多加入一個的功能,就是在shift鍵按下的狀況下,讓圖形的邊界是個正方形(取原邊界中較短的那條邊)。這個功能是由regulateShape(int,int)這個方法來完成的,它的代碼至關簡短,就很少作解釋了 。

 

  1. import java.awt.*;  
  2. import java.awt.event.MouseEvent;  
  3.   
  4. public abstract class RectBoundedShape implements IShape {  
  5.       
  6.     protected Color color;  
  7.     protected Stroke stroke;  
  8.     
  9.     protected int startX, startY, endX, endY;  
  10.       
  11.     protected RectBoundedShape(Color c, Stroke s, int x, int y) {  
  12.         color = c;  
  13.         stroke = s;  
  14.         startX = endX = x;  
  15.         startY = endY = y;  
  16.     }  
  17.       
  18.     public void processCursorEvent(MouseEvent e, int t) {  
  19.         if (t != IShape.CURSOR_DRAGGED)  
  20.             return;  
  21.         int x = e.getX();  
  22.         int y = e.getY();  
  23.         if (e.isShiftDown()) {  
  24.             regulateShape(x, y);  
  25.         } else {  
  26.             endX = x;  
  27.             endY = y;  
  28.         }  
  29.     }  
  30.       
  31.     protected void regulateShape(int x, int y) {  
  32.         int w = x - startX;  
  33.         int h = y - startY;  
  34.         int s = Math.min(Math.abs(w), Math.abs(h));  
  35.         if (s == 0) {  
  36.             endX = startX;  
  37.             endY = startY;  
  38.         } else {  
  39.             endX = startX + s * (w / Math.abs(w));  
  40.             endY = startY + s * (h / Math.abs(h));  
  41.         }  
  42.     }  
  43.       
  44. }  

 

有了RectBoundedShape這個父類打下的基礎,它下面的子類所要作的事情就是畫圖啦。全部子類的構造函數跟父類都是同樣的結構,基本上也都是直接調用super的構造函數,只是Diamond這個類爲了提升畫圖效率,「私下」定義了一個數組。RectBoundedShape的子類包括Line, Rect, Oval, 和Diamond。除了Diamond須要根據邊界長方形進行稍微計算求得菱形的四個點外,它們的圖形均可以直接利用Graphics類提供的方法很方便的畫出來,詳情能夠參看源代碼,就很少說了。如今看一下Line這個類。不一樣於其它幾個類,在shift鍵按下的狀況下,根據角度不一樣,咱們想畫出45度線,水平線,或者豎直線。因此,Line這個類不使用其父類定義的processCursorEvent(MouseEvent,int)方法,而是本身定義了一套。父類中regulateShape(int,int)方法的權限設成protected也是爲了給Line用的。代碼以下:

 

  1. public void processCursorEvent(MouseEvent e, int t) {  
  2.         if (t != IShape.CURSOR_DRAGGED)  
  3.             return;  
  4.         int x = e.getX();  
  5.         int y = e.getY();  
  6.         if (e.isShiftDown()) {  
  7.             //這個狀況單獨處理,否則就要除以0了  
  8.             if (x - startX == 0) { //豎直  
  9.                 endX = startX;  
  10.                 endY = y;  
  11.             } else {  
  12.                 //因爲對稱性,只要算斜率的絕對值  
  13.                 float slope = Math.abs(((float) (y - startY)) / (x - startX));  
  14.                 //小於30度,變成水平的  
  15.                 if (slope < 0.577) {  
  16.                     endX = x;  
  17.                     endY = startY;  
  18.                 //介於30度跟60度中間的,變成45度,利用父類的regulateShape(int,int)完成  
  19.                 } else if (slope < 1.155) {  
  20.                     regulateShape(x, y);  
  21.                 //大於60度,變成豎直的  
  22.                 } else {  
  23.                     endX = startX;  
  24.                     endY = y;  
  25.                 }  
  26.             }  
  27.         //若是shift鍵沒有按下,跟父類同樣處理  
  28.         } else {  
  29.             endX = x;  
  30.             endY = y;  
  31.         }  
  32.     }  

 

『PolyGon』

用戶畫多邊形的步驟是這樣的,先在一點按下鼠標左鍵,定義一個頂點,而後將鼠標拖拽到多邊形的下一個頂點,點鼠標右鍵將這個點記錄,以後重複這個步驟直到全部頂點都記錄,鬆開左鍵,多邊形完成。在多邊形完成前,顯示出來的不是閉合圖形,當左鍵鬆開時,圖形自動閉合。對於最後一個頂點,用戶不用點右鍵也會被自動記錄的。好了,來看一下這個過程是怎麼來完成的。方便起見,直接用註釋在代碼上解釋了。

 

  1. import java.awt.*;  
  2. import java.awt.event.MouseEvent;  
  3.   
  4. public class PolyGon implements IShape {  
  5.       
  6.     //相似於FreeShape和RectBoundedShape的變量  
  7.     private Color color;  
  8.     private Stroke stroke;  
  9.   
  10.     //記錄全部頂點座標,姑且稱之爲頂點集  
  11.     private PointsSet pointsSet;  
  12.     
  13.     //記錄多邊形是否完成。true表示完成  
  14.     private boolean finalized;  
  15.       
  16.     //記錄畫圖過程當中鼠標被拖拽到的點,姑且稱之爲浮點吧^_^  
  17.     private int currX, currY;  
  18.       
  19.     public PolyGon(Color c, Stroke s, int x, int y) {  
  20.         pointsSet = new PointsSet();  
  21.         color = c;  
  22.         stroke = s;  
  23.         pointsSet.addPoint(x, y);  
  24.         //剛開始先把浮點設置到起始頂點  
  25.         currX = x;  
  26.         currY = y;  
  27.         finalized = false;  
  28.     }  
  29.       
  30.     public void processCursorEvent(MouseEvent e, int t) {  
  31.         //首先更新浮點座標  
  32.         currX = e.getX();  
  33.         currY = e.getY();  
  34.         //右鍵按下時,將浮點加入到頂點集裏  
  35.         if (t == IShape.RIGHT_PRESSED) {  
  36.             pointsSet.addPoint(currX, currY);  
  37.         //左鍵按下時,設置多邊形到完成狀態,而且將浮點加入頂點集中  
  38.         } else if (t == IShape.LEFT_RELEASED) {  
  39.             finalized = true;  
  40.             pointsSet.addPoint(currX, currY);  
  41.         }  
  42.         /* 注意:上面的if-else結構只包含了RIGHT_PRESSED和LEFT_RELEASED兩種狀況, 
  43.            不過,這個方法也處理了CURSOR_DRAGGED這種狀況,就是更新浮點座標 */  
  44.     }  
  45.       
  46.     public void draw(Graphics2D g) {  
  47.         g.setColor(color);  
  48.         g.setStroke(stroke);  
  49.         if (finalized) {  
  50.             //一旦圖形完成,浮點就再也不用到了  
  51.             int[][] points = pointsSet.getPoints();  
  52.             int s = points[0].length;  
  53.             //這部分跟PolyLine相似  
  54.             if (s == 1) {  
  55.                 int x = points[0][0];  
  56.                 int y = points[1][0];  
  57.                 g.drawLine(x, y, x, y);  
  58.             } else {  
  59.                 g.drawPolygon(points[0], points[1], s);  
  60.             }  
  61.         } else { //圖形沒完成的狀況下,顯示的時候要用到浮點  
  62.             int[][] points = pointsSet.getPoints(currX, currY);  
  63.             g.drawPolyline(points[0], points[1], points[0].length);  
  64.         }  
  65.     }  
  66.           
  67. }  

 

『其餘』

DrawingBoard(extends JPanel)是附件程序中用的畫圖板類,它是在TestBoard類上的一個擴展,加入了其餘的模型類。另外,它提供了一些方法讓外部控制界面來設置繪圖色,畫圖板背景色,畫圖線條,橡皮擦大小(也是經過改變線條實現的)。這些就再也不一一贅述了。

AppFrame(extends JFrame)用來放畫圖板和控制面板。

此外,在稍微變更代碼的狀況下,還能夠加入新的圖形類,固然這些類要實現IShape接口,好比,直接繼承RectBoundedShape,定義新的圖形顯示代碼。

相關文章
相關標籤/搜索