本文講述一個(gè)畫圖板應(yīng)用程序的設(shè)計(jì),屏幕抓圖如下。這篇文章帶有三個(gè)附件,其中兩個(gè)jar文件都是j2sdk1.4.2_08編譯打包,包含源代碼,可執(zhí)行,如下表:
附件名稱及鏈接 |
詳情 |
jDraw_basic.jar |
本文是基于這個(gè)基本版本的,屏幕抓圖顯示的也是這個(gè)基本版本的界面。 |
jDraw_extended.jar |
在基礎(chǔ)版本上稍加擴(kuò)展,加入文件讀存功能,即可將所畫的圖存入一個(gè)模型文件(特定的格式,見下)或者從文件中讀取,也可以將其導(dǎo)出到一個(gè)PNG格式的文件。由于擴(kuò)展功能不是本文的重點(diǎn),并且也不復(fù)雜,所以文中就不在對(duì)其進(jìn)行闡述。它的源代碼只是在基本版本上增加了一些內(nèi)容。 |
jdraw_demo.zip |
屏幕抓圖中的圖形的模型文件,屬于純文本格式,為了節(jié)省空間,將其壓縮了一下,解壓縮取出其中的jdraw_demo.jdw文件后再使用。按理說(shuō),SGML/XML的格式才是正途,不過(guò)這只是個(gè)簡(jiǎn)單的應(yīng)用,不用那么大動(dòng)干戈了,就走個(gè)“邪道”吧:) |

『IShape』
這是所有圖形類(此后稱作模型類)都應(yīng)該實(shí)現(xiàn)接口,外部的控制類,比如畫圖板類就通過(guò)這個(gè)接口跟模型類“交流”。名字開頭的I表示它是一個(gè)接口(Interface),這是eclipse用的一個(gè)命名法則,覺得挺有用的,就借鑒來(lái)了。這個(gè)接口定義了兩個(gè)方法:
public void draw(java.awt.Graphics2D g);每個(gè)實(shí)現(xiàn)IShape的類都在這個(gè)方法里面指定它的圖形顯示代碼。public void processCursorEvent(java.awt.event.MouseEvent evt, int type);這個(gè)方法是在圖形(被用戶)繪制過(guò)程中,發(fā)生相關(guān)的鼠標(biāo)點(diǎn)擊和移動(dòng)事件時(shí)調(diào)用的。第一個(gè)參數(shù)就是所發(fā)生的鼠標(biāo)事件對(duì)象;第二個(gè)參數(shù)取值于IShape所定義的三個(gè)常數(shù):RIGHT_PRESSED, LEFT_RELEASED,和CURSOR_DRAGGED。
下面這個(gè)class diagram顯示了所有圖形類的結(jié)構(gòu)圖。FreeShape, RectBoundedShape,和PolyGon這三個(gè)類直接實(shí)現(xiàn)了IShape接口。其中,F(xiàn)reeShape和RectBoundedShape是抽象類,分別代表不規(guī)則圖形(比如鉛筆畫圖)和以一個(gè)長(zhǎng)方形為邊界的規(guī)則圖形,由于分屬于這兩個(gè)類別的圖形對(duì)于鼠標(biāo)事件的處理基本上都是一致的,所以就抽象出來(lái)這兩個(gè)父類,避免重復(fù)代碼。PolyGon是一個(gè)具體類,它的命名沒有采用Polygon是為了避免同java.awt.Polygon重名。它代表的圖形是多邊形,由于它獨(dú)特的鼠標(biāo)處理方式,它不屬于上面兩種類型圖形的任何一種,所以它直接實(shí)現(xiàn)了IShape接口。

IShape接口所定義的兩個(gè)方法到底是怎么被用到的呢?這個(gè)問題現(xiàn)在還不能立刻解答。在下面的部分,我們先講述FreeShape所定義的不規(guī)則圖形及其兩個(gè)具體子類PolyLine和Eraser,然后在這個(gè)基礎(chǔ)上講述一個(gè)縮略版的畫圖板類,到那個(gè)時(shí)候,上面問題的答案也就自然揭曉了。之后,我們?cè)倮^續(xù)講述其他的圖形類。
『FreeShape』
講到FreeShape,我們不得不先說(shuō)一下PointsSet這個(gè)類。這是一個(gè)util類,被FreeShape和PolyGon用到,代表一個(gè)有序的點(diǎn)集合,并提供方便的方法來(lái)加入新的點(diǎn)和讀取點(diǎn)坐標(biāo)。為了方便對(duì)模型類代碼的理解,這里列出PointsSet類的API。
public PointsSet();用默認(rèn)的初始容量(10)創(chuàng)建一個(gè)對(duì)象。public PointsSet(int initCap);用指定的初始容量(initCap)創(chuàng)建一個(gè)對(duì)象。public void addPoint(int x, int y);加入一個(gè)新的點(diǎn)到這個(gè)集合的末端;如果舊的末端點(diǎn)跟新的點(diǎn)重合,則不重復(fù)加入。public int[][] getPoints();將所有點(diǎn)以一個(gè)二維數(shù)組(int[2][n])返回。第一行是x坐標(biāo),第二行是y坐標(biāo)。public int[][] getPoints(int x, int y);類似上一個(gè)方法,只是最后將參數(shù)指定的點(diǎn)加在末尾(無(wú)論是否跟集合末端的點(diǎn)重合);這個(gè)方法只被PolyGon用到。
好了,來(lái)看下面代碼中FreeShape對(duì)IShape接口的實(shí)現(xiàn)。FreeShape有三個(gè)屬性變量:color, stroke,和pointsSet。權(quán)限設(shè)成protected當(dāng)然是給子類用啦。color就是色彩了,stroke用來(lái)指定使用線條的粗細(xì)(當(dāng)然,Stroke類的對(duì)象還可以指定交接點(diǎn)形狀之類的屬性,不過(guò)這里都使用其默認(rèn)值了),pointsSet當(dāng)然就是包含了所有控制點(diǎn)(這里叫控制點(diǎn)似乎不太恰當(dāng),因?yàn)槠鋵?shí)無(wú)法利用這些點(diǎn)來(lái)“控制”的,不過(guò)也想不到其他恰當(dāng)?shù)拿?,就這么叫吧)集合。值得注意的是構(gòu)造函數(shù)里面包含了起始點(diǎn)的坐標(biāo),這個(gè)點(diǎn)在函數(shù)里面被加到了控制點(diǎn)集中。
這類圖形對(duì)鼠標(biāo)事件的處理很簡(jiǎn)單,它只對(duì)IShape.CURSOR_DRAGGED類型的事件感興趣,每當(dāng)發(fā)生這類事件的時(shí)候,就把鼠標(biāo)拖拽到的新的點(diǎn)加入到控制點(diǎn)集中。當(dāng)然了,根據(jù)上面看到的PointsSet.addPoint(int,int)這個(gè)方法的“個(gè)性”,這個(gè)點(diǎn)是否真的被加入還要看它是否跟舊的末端點(diǎn)重合。
import java.awt.*;import java.awt.event.MouseEvent;public abstract class FreeShape implements IShape { protected Color color; protected Stroke stroke; protected PointsSet pointsSet; protected FreeShape(Color c, Stroke s, int x, int y) { pointsSet = new PointsSet(50); color = c; stroke = s; pointsSet.addPoint(x, y); } public void processCursorEvent(MouseEvent e, int t) { if (t != IShape.CURSOR_DRAGGED) return; pointsSet.addPoint(e.getX(), e.getY()); }}
FreeShape類沒有實(shí)現(xiàn)IShape接口的draw(Graphics2D)方法,很明顯,這個(gè)方法是留給子類來(lái)完成的。PolyLine和Eraser繼承了FreeShape,分別代表鉛筆繪出的圖形和橡皮擦。其中PolyLine的構(gòu)造函數(shù)結(jié)構(gòu)跟其父類相似,直接調(diào)用父類的super方法來(lái)完成;相比之下,Eraser類就有點(diǎn)“叛逆”了,它的參數(shù)里面用一個(gè)JComponent替換了Color。Eraser類是通過(guò)畫出跟畫圖板背景色彩一致的線條來(lái)掩蓋原有圖形而實(shí)現(xiàn)橡皮擦的效果的,但由于畫圖板的背景色是可以調(diào)的(見抓圖的Color Settings部分),直接給Eraser的構(gòu)造函數(shù)一個(gè)色彩對(duì)象不太合適,所以干脆將畫圖板自己(JComponent)傳了進(jìn)來(lái),這樣,每次Eraser設(shè)定圖形色彩時(shí),都直接問畫圖板要它的背景色。來(lái)看一下PolyLine對(duì)draw(Graphics2D)方法的實(shí)現(xiàn):
public void draw(Graphics2D g) { g.setColor(color); g.setStroke(stroke); int[][] points = pointsSet.getPoints(); int s = points[0].length; if (s == 1) { int x = points[0][0]; int y = points[1][0]; g.drawLine(x, y, x, y); } else { g.drawPolyline(points[0], points[1], s); } }
這個(gè)方法里面有一個(gè)if-else結(jié)構(gòu),由于構(gòu)造函數(shù)里面已經(jīng)將起始點(diǎn)加入控制點(diǎn)集中,所以pointsSet.getPoints()會(huì)至少返回一個(gè)點(diǎn)。利用Graphics.drawPolyline(int[],int[],int)畫圖時(shí),如果只有一個(gè)點(diǎn),它是不會(huì)畫出來(lái)東西的,所以檢查一下點(diǎn)數(shù),如果只有一個(gè),則改用Graphics.drawLine(int,int,int,int)將這個(gè)點(diǎn)畫出來(lái)。Eraser的draw(Graphics2D)方法跟上面基本上完全一樣,只是傳給Graphics.setColor(Color)的參數(shù)是通過(guò)JComponent.getBackground()得到的。
『TestBoard』
現(xiàn)在就來(lái)看一個(gè)精簡(jiǎn)版的畫圖板類:TestBoard。下面的代碼,是通過(guò)代碼注釋進(jìn)行解釋的。需要注意的是,TestBoard本身還不能直接運(yùn)行,需要把它放到一個(gè)JFrame里面才行。同時(shí)畫圖工具的切換也需要外部的控件來(lái)處理。不過(guò)這些都比較簡(jiǎn)單了,就不多說(shuō)了。
import java.awt.*;import java.awt.event.*;import javax.swing.*;import java.util.ArrayList;public class TestBoard extends JPanel implements MouseListener, MouseMotionListener { //定義一些常量 public static final int TOOL_PENCIL = 1; public static final int TOOL_ERASER = 2; public static final Stroke STROKE = new BasicStroke(1.0f); public static final Stroke ERASER_STROKE = new BasicStroke(15.0f); private ArrayList shapes; //保存所有的圖形對(duì)象(IShape) private IShape currentShape; //指向當(dāng)前還未完成的圖形 private int tool; //代表當(dāng)前使用的畫圖工具(TOOL_PENCIL或TOOL_ERASER) public TestBoard() { //進(jìn)行一些初始化 shapes = new ArrayList(); tool = TOOL_PENCIL; currentShape = null; //安裝鼠標(biāo)監(jiān)聽器 addMouseListener(this); addMouseMotionListener(this); } //外部的控制界面可以通過(guò)這個(gè)方法切換畫圖工具 public void setTool(int t) { tool = t; } //override JPanel的方法。通過(guò)調(diào)用IShape.draw(Graphics2D)方法來(lái)顯示圖形 protected void paintComponent(Graphics g) { super.paintComponent(g); int size = shapes.size(); Graphics2D g2d = (Graphics2D) g; for (int i=0; i<size; i++) { ((IShape) shapes.get(i)).draw(g2d); } } public void mousePressed(MouseEvent e) { /* 當(dāng)左鍵點(diǎn)擊時(shí),currentShape肯定指向null。根據(jù)當(dāng)前畫圖工具創(chuàng)建相應(yīng)圖形對(duì)象, 將currentShape指向它,并把這個(gè)對(duì)象加入到對(duì)象集合(shapes)中。另外,調(diào)用 repaint()方法將畫圖板的畫面更新一下。 */ if (e.getButton() == MouseEvent.BUTTON1) { switch (tool) { case TOOL_PENCIL: currentShape = new PolyLine(getForeground(), STROKE, e.getX(), e.getY()); break; case TOOL_ERASER: currentShape = new Eraser(this, ERASER_STROKE, e.getX(), e.getY()); break; } shapes.add(currentShape); repaint(); /* 當(dāng)右鍵點(diǎn)擊并且currentShape不指向null時(shí),調(diào)用currentShape的 processCursorEvent(MouseEvent,int)方法,類型參數(shù)是 IShape.RIGHT_PRESSED。 repaint()*/ } else if (e.getButton() == MouseEvent.BUTTON3 && currentShape != null) { currentShape.processCursorEvent(e, IShape.RIGHT_PRESSED); repaint(); } } public void mouseDragged(MouseEvent e) { /* 當(dāng)鼠標(biāo)拖拽并且currentShape不指向null時(shí)(這種情況下,左鍵肯定處于 按下狀態(tài)),調(diào)用currentShape的processCursorEvent(MouseEvent,int)方法, 類型參數(shù)是IShape.CURSOR_DRAGGED。 repaint()*/ if (currentShape != null) { currentShape.processCursorEvent(e, IShape.CURSOR_DRAGGED); repaint(); } } public void mouseReleased(MouseEvent e) { /* 當(dāng)左鍵被松開并且currentShape不指向null時(shí)(這個(gè)時(shí)候,currentShape 肯定不會(huì)指向null的,多檢查一次,保險(xiǎn)),調(diào)用currentShape的 processCursorEvent(MouseEvent,int)方法,類型參數(shù)是 IShape.CURSOR_DRAGGED。 repaint()*/ if (e.getButton() == MouseEvent.BUTTON1 && currentShape != null) { currentShape.processCursorEvent(e, IShape.LEFT_RELEASED); currentShape = null; repaint(); } } //對(duì)下面這些事件不感興趣 public void mouseClicked(MouseEvent e) {} public void mouseEntered(MouseEvent e) {} public void mouseExited(MouseEvent e) {} public void mouseMoved(MouseEvent e) {} }
至此,整個(gè)程序的流程就很清楚了,文章開頭部分的問題也被解開了。接下來(lái),就繼續(xù)來(lái)看其他的模型類。
『RectBoundedShape』
RectBoundedShape構(gòu)造函數(shù)的結(jié)構(gòu)跟FreeShape一樣,在色彩和線條的運(yùn)用上也是一樣的,也只對(duì)鼠標(biāo)拖拽事件感興趣。不過(guò),它只有兩個(gè)控制點(diǎn),起始點(diǎn)和結(jié)束點(diǎn),所以,不需要用到PointsSet。本來(lái),RectBoundedShape這個(gè)類是比FreeShape簡(jiǎn)單的,在處理鼠標(biāo)拖拽事件時(shí)只要將結(jié)束點(diǎn)設(shè)置到新拖拽到的點(diǎn)就可以了。不過(guò),這里我們多加入一個(gè)的功能,就是在shift鍵按下的情況下,讓圖形的邊界是個(gè)正方形(取原邊界中較短的那條邊)。這個(gè)功能是由regulateShape(int,int)這個(gè)方法來(lái)完成的,它的代碼相當(dāng)簡(jiǎn)短,就不多做解釋了 。
import java.awt.*;import java.awt.event.MouseEvent;public abstract class RectBoundedShape implements IShape { protected Color color; protected Stroke stroke; protected int startX, startY, endX, endY; protected RectBoundedShape(Color c, Stroke s, int x, int y) { color = c; stroke = s; startX = endX = x; startY = endY = y; } public void processCursorEvent(MouseEvent e, int t) { if (t != IShape.CURSOR_DRAGGED) return; int x = e.getX(); int y = e.getY(); if (e.isShiftDown()) { regulateShape(x, y); } else { endX = x; endY = y; } } protected void regulateShape(int x, int y) { int w = x - startX; int h = y - startY; int s = Math.min(Math.abs(w), Math.abs(h)); if (s == 0) { endX = startX; endY = startY; } else { endX = startX + s * (w / Math.abs(w)); endY = startY + s * (h / Math.abs(h)); } } }
有了RectBoundedShape這個(gè)父類打下的基礎(chǔ),它下面的子類所要做的事情就是畫圖啦。所有子類的構(gòu)造函數(shù)跟父類都是一樣的結(jié)構(gòu),基本上也都是直接調(diào)用super的構(gòu)造函數(shù),只是Diamond這個(gè)類為了提高畫圖效率,“私下”定義了一個(gè)數(shù)組。RectBoundedShape的子類包括Line, Rect, Oval, 和Diamond。除了Diamond需要根據(jù)邊界長(zhǎng)方形進(jìn)行稍微計(jì)算求得菱形的四個(gè)點(diǎn)外,它們的圖形都可以直接利用Graphics類提供的方法很方便的畫出來(lái),詳情可以參看源代碼,就不多說(shuō)了?,F(xiàn)在看一下Line這個(gè)類。不同于其它幾個(gè)類,在shift鍵按下的情況下,根據(jù)角度不同,我們想畫出45度線,水平線,或者豎直線。所以,Line這個(gè)類不使用其父類定義的processCursorEvent(MouseEvent,int)方法,而是自己定義了一套。父類中regulateShape(int,int)方法的權(quán)限設(shè)成protected也是為了給Line用的。代碼如下:
public void processCursorEvent(MouseEvent e, int t) { if (t != IShape.CURSOR_DRAGGED) return; int x = e.getX(); int y = e.getY(); if (e.isShiftDown()) { //這個(gè)情況單獨(dú)處理,不然就要除以0了 if (x - startX == 0) { //豎直 endX = startX; endY = y; } else { //由于對(duì)稱性,只要算斜率的絕對(duì)值 float slope = Math.abs(((float) (y - startY)) / (x - startX)); //小于30度,變成水平的 if (slope < 0.577) { endX = x; endY = startY; //介于30度跟60度中間的,變成45度,利用父類的regulateShape(int,int)完成 } else if (slope < 1.155) { regulateShape(x, y); //大于60度,變成豎直的 } else { endX = startX; endY = y; } } //如果shift鍵沒有按下,跟父類一樣處理 } else { endX = x; endY = y; } }
『PolyGon』
用戶畫多邊形的步驟是這樣的,先在一點(diǎn)按下鼠標(biāo)左鍵,定義一個(gè)頂點(diǎn),然后將鼠標(biāo)拖拽到多邊形的下一個(gè)頂點(diǎn),點(diǎn)鼠標(biāo)右鍵將這個(gè)點(diǎn)記錄,之后重復(fù)這個(gè)步驟直到所有頂點(diǎn)都記錄,松開左鍵,多邊形完成。在多邊形完成前,顯示出來(lái)的不是閉合圖形,當(dāng)左鍵松開時(shí),圖形自動(dòng)閉合。對(duì)于最后一個(gè)頂點(diǎn),用戶不用點(diǎn)右鍵也會(huì)被自動(dòng)記錄的。好了,來(lái)看一下這個(gè)過(guò)程是怎么來(lái)完成的。方便起見,直接用注釋在代碼上解釋了。
import java.awt.*;import java.awt.event.MouseEvent;public class PolyGon implements IShape { //類似于FreeShape和RectBoundedShape的變量 private Color color; private Stroke stroke; //記錄所有頂點(diǎn)坐標(biāo),姑且稱之為頂點(diǎn)集 private PointsSet pointsSet; //記錄多邊形是否完成。true表示完成 private boolean finalized; //記錄畫圖過(guò)程中鼠標(biāo)被拖拽到的點(diǎn),姑且稱之為浮點(diǎn)吧^_^ private int currX, currY; public PolyGon(Color c, Stroke s, int x, int y) { pointsSet = new PointsSet(); color = c; stroke = s; pointsSet.addPoint(x, y); //剛開始先把浮點(diǎn)設(shè)置到起始頂點(diǎn) currX = x; currY = y; finalized = false; } public void processCursorEvent(MouseEvent e, int t) { //首先更新浮點(diǎn)坐標(biāo) currX = e.getX(); currY = e.getY(); //右鍵按下時(shí),將浮點(diǎn)加入到頂點(diǎn)集里 if (t == IShape.RIGHT_PRESSED) { pointsSet.addPoint(currX, currY); //左鍵按下時(shí),設(shè)置多邊形到完成狀態(tài),并且將浮點(diǎn)加入頂點(diǎn)集中 } else if (t == IShape.LEFT_RELEASED) { finalized = true; pointsSet.addPoint(currX, currY); } /* 注意:上面的if-else結(jié)構(gòu)只包含了RIGHT_PRESSED和LEFT_RELEASED兩種情況, 不過(guò),這個(gè)方法也處理了CURSOR_DRAGGED這種情況,就是更新浮點(diǎn)坐標(biāo) */ } public void draw(Graphics2D g) { g.setColor(color); g.setStroke(stroke); if (finalized) { //一旦圖形完成,浮點(diǎn)就不再用到了 int[][] points = pointsSet.getPoints(); int s = points[0].length; //這部分跟PolyLine類似 if (s == 1) { int x = points[0][0]; int y = points[1][0]; g.drawLine(x, y, x, y); } else { g.drawPolygon(points[0], points[1], s); } } else { //圖形沒完成的情況下,顯示的時(shí)候要用到浮點(diǎn) int[][] points = pointsSet.getPoints(currX, currY); g.drawPolyline(points[0], points[1], points[0].length); } } }
『其他』
DrawingBoard(extends JPanel)是附件程序中用的畫圖板類,它是在TestBoard類上的一個(gè)擴(kuò)展,加入了其他的模型類。另外,它提供了一些方法讓外部控制界面來(lái)設(shè)置繪圖色,畫圖板背景色,畫圖線條,橡皮擦大小(也是通過(guò)改變線條實(shí)現(xiàn)的)。這些就不再一一贅述了。
AppFrame(extends JFrame)用來(lái)放畫圖板和控制面板。
此外,在稍微變動(dòng)代碼的情況下,還可以加入新的圖形類,當(dāng)然這些類要實(shí)現(xiàn)IShape接口,比如,直接繼承RectBoundedShape,定義新的圖形顯示代碼。