Java 從零開始實現一個畫圖板、以及影像處理功能,程式碼可復現
這是一個學習分享部落格,帶你從零開始實現一個畫圖板、影像處理的小專案,為了降低閱讀難度,本部落格將畫圖板的一步步迭代優化過程展示給讀者,篇幅較長,Java初學者可放心食用。(文末有原始碼)
本部落格實現的功能(根據本文講解的順序)
- 直線、簽字筆、實時直線、謝爾賓斯基地毯、遞迴KLine、矩形、圓、實心矩形、實心圓、等腰三角形、三角形、多邊形、改進多邊形、 立方體、橡皮擦
- 畫筆的顏色更改
- 撤回、儲存、開啟
- 開啟jpg圖片,儲存圖片
- 圖片特效:馬賽克、黑白照、油畫、背景替換、圖片融合等等;
- 圖片的放大和縮小,圖片旋轉
- 圖片的顏色調整
介面效果
怎麼樣?如果覺得還不錯的話就請繼續看下去吧!
首先我們要寫一個介面,就要給介面新增一個監聽器,對監聽器不太熟悉的同學,可以看我的這篇文章 常見監聽器用法
第一步:建立畫布
- 萬事開頭難,我們從建立一個窗體開始,並給窗體新增畫筆g。
package drawBoard_test;
import javax.swing.*;
import java.awt.*;
public class DrawUI extends JFrame {
String[] strs = {"直線","簽字筆","實時直線", "謝爾賓斯基地毯","遞迴KLine","矩形", "圓", "實心矩形", "實心圓", "等腰三角形", "三角形", "多邊形",
"改進多邊形","立方體", "橡皮擦", "撤回", "儲存", "開啟"};
Color[] color = {Color.red,Color.white,Color.black,Color.blue};
//新增功能和顏色按鈕
public void addButton(){
for(String str : strs){
JButton btn = new JButton(str);
add(btn);
}
Dimension dim = new Dimension(30,30);
for(Color c : color){
JButton btn = new JButton();
btn.setBackground(c);
btn.setPreferredSize(dim);
add(btn);
}
}
public void initUI(){
this.setTitle("畫圖板");
FlowLayout flow = new FlowLayout();
this.setLayout(flow);
this.setSize(1000,800);
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
this.addButton();
this.setVisible(true);
Graphics g = getGraphics();
}
public static void main(String[] args) {
DrawUI drawUI = new DrawUI();
drawUI.initUI();
}
}
- 大家可以試著執行一下,出現以下效果,第一步就算成功了
第二步:為窗體和按鈕新增監聽器
- 首先要建立一個監聽器,我們需要用到事件監聽器ActionListener,和滑鼠監聽器MouseListener,MouseMotionListener,
所以我們選擇繼承這三個介面, 我們都知道,繼承一個介面時需要重寫介面的所有方法,但是我們又不會使用到三個介面的所有方法(滑鼠進入/離開元件),
所以我們可以先寫一個類A繼承所有介面,然後再用監聽器類去繼承類A。 - 監聽器的父類:
package drawBoard_test;
import java.awt.event.*;
public class DrawListenerFather implements ActionListener, MouseListener, MouseMotionListener {
@Override
public void actionPerformed(ActionEvent e) {
}
@Override
public void mouseClicked(MouseEvent e) {
}
@Override
public void mousePressed(MouseEvent e) {
}
@Override
public void mouseReleased(MouseEvent e) {
}
@Override
public void mouseEntered(MouseEvent e) {
}
@Override
public void mouseExited(MouseEvent e) {
}
@Override
public void mouseDragged(MouseEvent e) {
}
@Override
public void mouseMoved(MouseEvent e) {
}
}
接下來,建立我們需要的監聽器DrawListener,我們如果想在畫圖板上繪製的話,需要將主頁面的畫筆g傳給監聽器,
所以我們給監聽器新增成員變數Graphic g;並新增setG()方法。
package drawBoard_test;
public class DrawListener extends DrawListenerFather {
private Graphics g;
public void setG(Graphics g) {
this.g = g;
}
@Override
public void actionPerformed(ActionEvent e) {
}
@Override
public void mouseClicked(MouseEvent e) {
}
@Override
public void mousePressed(MouseEvent e) {
}
@Override
public void mouseReleased(MouseEvent e) {
}
@Override
public void mouseEntered(MouseEvent e) {
}
@Override
public void mouseDragged(MouseEvent e) {
}
@Override
public void mouseMoved(MouseEvent e) {
}
}
- 我們將主窗體的畫筆g傳給監聽器,併為主窗體以及它的所有按鈕以及加上監聽器。
- 主窗體DrawUI中的程式碼更新為:
package drawBoard_test;
import javax.swing.*;
import java.awt.*;
public class DrawUI extends JFrame {
DrawListener dl = new DrawListener();
String[] strs = {"直線","簽字筆","實時直線", "謝爾賓斯基地毯","遞迴KLine","矩形", "圓", "實心矩形", "實心圓", "等腰三角形", "三角形", "多邊形",
"改進多邊形","立方體", "橡皮擦", "撤回", "儲存", "開啟"};
Color[] color = {Color.red,Color.white,Color.black,Color.blue};
public void addButton(){
for(String str : strs){
JButton btn = new JButton(str);
btn.addActionListener(dl); //新增事件監聽器
add(btn);
}
Dimension dim = new Dimension(30,30);
for(Color c : color){
JButton btn = new JButton();
btn.setBackground(c);
btn.setPreferredSize(dim);
btn.addActionListener(dl); //新增事件監聽器
add(btn);
}
}
public void initUI(){
this.setTitle("畫圖板");
FlowLayout flow = new FlowLayout();
this.setLayout(flow);
this.setSize(1000,800);
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
this.addButton();
this.setVisible(true);
this.addMouseListener (dl);
this.addMouseMotionListener (dl);//新增滑鼠監聽器
Graphics g = getGraphics ();
dl.setG(g); //將窗體的畫筆g傳入監聽器
}
public static void main(String[] args) {
DrawUI drawUI = new DrawUI();
drawUI.initUI();
}
}
接下來我們就可以去實現我們的繪圖功能了!
第三步,完善監聽器的功能
我們在監聽器中建立一個字串shapeName,當點選按鈕時,將按鈕上的字元賦給shapeName,再根據shapeName的值來決定滑鼠監聽器的具體行為
繪製直線以及更換畫筆顏色
- 繪製直線我們只需要知道滑鼠點選時的座標和滑鼠釋放時的座標,然後使用g.drawLine(x1,y1,x2,y2)即可繪製成功
- 我們來看程式碼
package drawBoard_test;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.MouseEvent;
public class DrawListener extends DrawListenerFather {
private Graphics g;
String shapeName = null; //按鈕上的圖形名稱
String btn_action ; //按鈕上的字串
Color color; //記錄當前畫筆的顏色
int x2,y2,x3,y3; //存放座標
public void setG(Graphics g) {
this.g = g;
}
@Override
public void actionPerformed(ActionEvent e) {
/**
*有的小夥伴可能會有疑問,為什麼要用btn_action做一箇中間量呢?試想一下,如果我們直接使用switch(shapeName),那麼我們點選顏色按鈕的時候
* shapeName就會被換成空值"",我們就需要重新點選圖形按鈕再進行繪製。
*/
btn_action = e.getActionCommand();
if(btn_action.equals("")){
JButton btn = (JButton) e.getSource(); //getSource方法獲取觸發此次事件的元件物件,返回值為Object型別
color = btn.getBackground(); //獲取按鈕元件的背景顏色
g.setColor(color);
return;
}else {
shapeName = btn_action;
}
}
@Override
public void mousePressed(MouseEvent e) {
x2 = e.getX();
y2 = e.getY();
}
@Override
public void mouseReleased(MouseEvent e) {
x3 = e.getX();
y3 = e.getY();
if(shapeName == null) return;
switch(shapeName){
case "直線":
g.drawLine(x2, y2, x3, y3);
break;
}
}
}
此時,畫圖板可以繪製出直線,我們來看一下效果
實現直線的繪製之後,其餘功能的實現也是水到渠成的,我們繼續往下看。
矩形、圓、實心矩形、實心圓、等腰三角形、謝爾賓斯基地毯、遞迴KLine、立方體、橡皮擦功能以及顏色按鈕的實現
- 矩形:矩形的實現使用g.drawRext(x2,y2,x2-x3,y2-y3)繪製,需要一個座標,和長、寬。
- 我們可以直接使用上面的式子繪製,但是如果我們從左下往右上拖動滑鼠時,就無法繪出矩形
所以我們左上角的座標的x,y座標使用兩點中較小的x,y值,長寬取差的絕對值,即
g.drawRect(Math.min(x2,x3),Math.min(y2,y3),Math.abs(x3-x2),Math.abs(y3-y2));
- 我們可以直接使用上面的式子繪製,但是如果我們從左下往右上拖動滑鼠時,就無法繪出矩形
- 圓:圓的引數與矩形相同 g.drawOval(x2,y2,x2-x3,y2-y3) ,畫出的圓為同樣引數畫出的矩形的內切矩形
- 實心矩形:g.fillRect(Math.min(x2,x3),Math.min(y2,y3),Math.abs(x3-x2),Math.abs(y3-y2));
- 實心圓:g.fillOval(Math.min(x2,x3),Math.min(y2,y3),Math.abs(x3-x2),Math.abs(y3-y2));
- 等腰三角形:等腰三角形的實現是用三條直線進行連線,我們用矩形作為參考,拖動滑鼠獲得的矩形,取矩形的下邊兩個點
和上邊線的中點進行連線,即可獲得一個等腰三角形 - 謝爾賓斯基地毯:這是一個依靠遞迴實現的圖形,將一個實心正方形劃分為的9個小正方形,去掉中間的小正方形,
再對餘下的小正方形重複這一操作便能得到謝爾賓斯基地毯。實現結果如圖所示
- 遞迴KLine:我們炒股的曲線往往是曲折蜿蜒的,我們就來模擬一下這種曲線,我們通過滑鼠的拖動可以獲得它的起始和終止的位置座標,
然後我們取他們的中點的x座標,和範圍內隨機的y座標,重複這一操作,直到兩點x座標相鄰時就連線。 - 立方體:使用斜二側畫法確定頂點座標,然後進行連線
- 橡皮擦:橡皮擦是顏色與背景顏色相同的矩形。
根據上述的描述,我們將監聽器的程式碼更新為
package drawBoard_test;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.MouseEvent;
public class DrawListener extends DrawListenerFather {
private Graphics g;
String shapeName = null;
String btn_action ;
Color color;
int x2,y2,x3,y3;
public void setG(Graphics g) {
this.g = g;
}
@Override
public void actionPerformed(ActionEvent e) {
btn_action = e.getActionCommand(); //btn_action
if(btn_action.equals("")){
JButton btn = (JButton) e.getSource();
color = btn.getBackground();
g.setColor(color);
return;
}else {
shapeName = btn_action;
}
}
@Override
public void mousePressed(MouseEvent e) {
x2 = e.getX();
y2 = e.getY();
}
@Override
public void mouseReleased(MouseEvent e) {
x3 = e.getX();
y3 = e.getY();
if(shapeName == null) return;
switch(shapeName){
case "直線":
g.drawLine(x2, y2, x3, y3);
break;
case "矩形":
g.drawRect(Math.min(x2,x3),Math.min(y2,y3),Math.abs(x3-x2),Math.abs(y3-y2));
break;
case "圓" :
g.drawOval(Math.min(x2,x3),Math.min(y2,y3),Math.abs(x3-x2),Math.abs(y3-y2));
break;
case "謝爾賓斯基地毯" :
Sierpinski(Math.min(x2,x3),Math.min(y2,y3),Math.abs(x3-x2),Math.abs(y3-y2));
break;
case "遞迴KLine" :
KLine(x2,y2,x3,y3,y3-y2);
break;
case "實心矩形" :
g.fillRect(Math.min(x2,x3),Math.min(y2,y3),Math.abs(x3-x2),Math.abs(y3-y2));
break;
case "實心圓" :
g.fillOval(Math.min(x2,x3),Math.min(y2,y3),Math.abs(x3-x2),Math.abs(y3-y2));
break;
case "等腰三角形" :
g.drawLine(x2,y3,x3,y3);
g.drawLine(x2,y3,(x2+x3)/2,y2);
g.drawLine(x3,y3,(x2+x3)/2,y2);
break;
case "立方體" :
g.drawRect(Math.min(x2,x3),Math.min(y2,y3),Math.abs(x3-x2),Math.abs(y3-y2));
g.drawLine(x2+(int)((x3-x2)*1.414/4),y2-(int)((y3-y2)*1.414/4),x2,y2);
g.drawLine(x2+(int)((x3-x2)*1.414/4),y2-(int)((y3-y2)*1.414/4),x3+(int)((x3-x2)*1.414/4),y2-(int)((y3-y2)*1.414/4));
g.drawLine(x3,y2,x3+(int)((x3-x2)*1.414/4),y2-(int)((y3-y2)*1.414/4));
g.drawLine(x3+(int)((x3-x2)*1.414/4),y3-(int)((y3-y2)*1.414/4),x3+(int)((x3-x2)*1.414/4),y2-(int)((y3-y2)*1.414/4));
g.drawLine(x3+(int)((x3-x2)*1.414/4),y3-(int)((y3-y2)*1.414/4),x3,y3);
break;
case "橡皮擦" :
Color pre = g.getColor(); //記錄之前的顏色 ,用完再換回去
g.setColor( new JButton().getBackground());
g.fillRect(Math.min(x2,x3),Math.min(y2,y3),Math.abs(x3-x2),Math.abs(y3-y2));
g.setColor(pre);
break;
}
}
//遞迴KLine
public void KLine(int x1 , int y1 , int x2 , int y2, int x){
if(Math.abs(x2-x1)<= 1 || Math.abs(y2-y1) <= 1 || x < 1){
g.drawLine(x1, y1, x2, y2);
specialList.add(new Point(x1,y1));
return;
}
Random random = new Random(0);
int ran = random.nextInt(x);
int mid = ((y2+y1)/2-x+ran*2);
x = (int)(x*0.618);
KLine(x1, y1, (x1+x2)/2, mid,x);
KLine((x1+x2)/2, mid, x2,y2,x);
}
//謝爾賓斯基地毯
public void Sierpinski(int x,int y,int w,int h){
if(w>0&&h>0){
g.fillRect(x+w/3,y+h/3,w/3,h/3);
Sierpinski(x,y,w/3,h/3);
Sierpinski(x+w/3,y,w/3,h/3);
Sierpinski(x+2*w/3,y,w/3,h/3);
Sierpinski(x,y+h/3,w/3,h/3);
Sierpinski(x+2*w/3,y+h/3,w/3,h/3);
Sierpinski(x,y+2*h/3,w/3,h/3);
Sierpinski(x+w/3,y+2*h/3,w/3,h/3);
Sierpinski(x+2*w/3,y+2*h/3,w/3,h/3);
}
}
}
較複雜一點的圖形功能:簽字筆、實時直線、三角形、多邊形、改進多邊形的實現
- 簽字筆:滑鼠拖動時一直獲取座標,並將這個座標與上一個座標連線
- 實時直線:滑鼠按下時獲取一個座標,然後拖動時獲取實時座標連線,並將上一條線用一條背景色的直線覆蓋。
- 三角形:滑鼠點選時獲取座標①,再次點選獲取座標②,並將①②連線,再次點選獲取座標③,並將①③,②③連線。
- 多邊形:第一次點選獲取座標①,此後每次點選獲取座標n,並連線座標n和前一次點選獲取的座標,最後點選右鍵,連線座標①和最後一次左鍵點選的座標
- 改進多邊形:滑鼠點選n次,然後用這個n個點作為頂點,畫出一個多邊形。
- 由於簽字筆、三角形、多邊形、改進多邊形的實現比較複雜,所以我們將他們作為一個獨立的類來寫,我們的程式碼也更容易擴充和維護。
此時,我們的監聽器的程式碼更新為 - 三角形類
package drawBoard_test;
import java.awt.*;
import java.awt.event.MouseEvent;
public class Triangle {
static int x1,y1,x2,y2,x3,y3; //對應三角形的三個點
static int num; //作為已經點了幾個點的控制訊號
public void drawTriangle(MouseEvent e , Graphics g){
if(num == 0){
x1 = e.getX();
y1 = e.getY();
num++;
}else if(num == 1){
x2 = e.getX();
y2 = e.getY();
g.drawLine(x1,y1,x2,y2);
num++;
}else if(num == 2){
x3 = e.getX();
y3 = e.getY();
g.drawLine(x3,y3,x2,y2);
g.drawLine(x3,y3,x1,y1);
num=0;
}
}
}
- 多邊形類
package drawBoard_test;
import java.awt.*;
import java.awt.event.MouseEvent;
public class Polygon {
static int x1,y1,x2,y2,x3,y3;
static int num;
public void drawPolygon(MouseEvent e , Graphics g){
if(num == 0){
x1 = e.getX();
y1 = e.getY();
num++;
}else if(num == 1){
x2 = e.getX();
y2 = e.getY();
g.drawLine(x1,y1,x2,y2);
num++;
}else if (num == 2){
if(e.getButton()==3){
g.drawLine(x1,y1,x2,y2);
num=0;
return;
}
x3 = e.getX();
y3 = e.getY();
g.drawLine(x3,y3,x2,y2);
num++;
}else if(num == 3){
if(e.getButton()==3){
g.drawLine(x1,y1,x3,y3);
num=0;
return;
}
x2 = e.getX();
y2 = e.getY();
g.drawLine(x3,y3,x2,y2);
num--;
}
}
}
- 改進多邊形類
package drawBoard_test;
import java.awt.*;
import java.util.ArrayList;
public class PolygonPro {
//挑選x座標最大的點作為基準點,計算其餘點與基準點的正切值,根據正切值從大到小依次連線,得到一個多邊形。
public void drawPolygonPro(ArrayList<Point> list, Graphics g){
if(list.size() == 0||list.size() == 1||list.size() == 2) return;
int right = findRight(list);
System.out.println(right);
Point rightPoint = new Point(list.get(right).x, list.get(right).y);
list.remove(right);
double[] tan = new double[list.size()];
for (int i = 0; i < list.size(); i++) {
tan[i] = ((double) rightPoint.y-list.get(i).y)/(rightPoint.x-list.get(i).x);
}
int pre;
int cur = indexOfMax(tan);
g.drawLine(list.get(cur).x,list.get(cur).y, rightPoint.x, rightPoint.y);
tan[cur] = Integer.MIN_VALUE;
for (int i = 0; i < tan.length-1; i++) {
pre = cur;
cur = indexOfMax(tan);
g.drawLine(list.get(pre).x,list.get(pre).y, list.get(cur).x,list.get(cur).y);
tan[cur] = Integer.MIN_VALUE;
}
g.drawLine(list.get(cur).x,list.get(cur).y, rightPoint.x, rightPoint.y);
}
private int findRight(ArrayList<Point> list) {
int result = 0;
for (int i = 1; i < list.size(); i++) {
result = list.get(i).x>list.get(result).x?i:result;
}
return result ;
}
//返回陣列中的最大值的下標
private int indexOfMax(double[] tan){
int v= 0 ;
for(int i = 1 ; i < tan.length; i ++){
v = tan[i]>tan[v]?i:v;
}
return v;
}
}
- 簽字筆類
package drawBoard_test;
import java.awt.*;
import java.awt.event.MouseEvent;
public class Pen {
public static int x1,y1,x2,y2;
public static int state = 1;
public void draw(MouseEvent e , Graphics g) {
switch(state){
case 1 :
x1 = e.getX();
y1 = e.getY();
state = 2;
break;
case 2 :
x2 = e.getX();
y2 = e.getY();
g.drawLine(x2,y2,x1,y1);
state = 3;
break;
case 3 :
x1 = e.getX();
y1 = e.getY();
g.drawLine(x2,y2,x1,y1);
state = 2;
break;
}
}
}
- 實時直線類
package drawBoard_test;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseEvent;
public class RealLine {
public static int x1,y1,x2,y2,x3,y3;
public void draw(MouseEvent e , Graphics g){
Color pre = g.getColor();
g.setColor( new JButton().getBackground());
if(x2 !=0 ){
g.drawLine(x2,y2,x1,y1);
}
g.setColor(pre);
x3 = e.getX();
y3 = e.getY();
g.drawLine(x3,y3,x1,y1);
x2=x3;
y2=y3;
}
}
監聽器DrawListener中的程式碼可以參考以下程式碼
ArrayList<Point> list = new ArrayList<>();//用於存放改進多邊形的所有的頂點。
@Override
public void mouseClicked(MouseEvent e) {
if(shapeName == null) return;
switch(shapeName){
case "三角形" :
new Triangle().drawTriangle(e,g);
break;
case "多邊形":
new Polygon().drawPolygon(e,g);
break;
case "改進多邊形":
if(e.getButton()==3){
new PolygonPro().drawPolygonPro(list,g);
list.clear();
break;
}else{
Point point = new Point(e.getX(),e.getY());
list.add(point);
break;
}
default:
break;
}
}
@Override
public void mouseDragged(MouseEvent e) {
if(shapeName == null) return;
switch (shapeName){
case "實時直線":
new RealLine().draw(e,g);
break;
case "簽字筆":
new Pen().draw(e,g);
break;
}
}
@Override
public void mousePressed(MouseEvent e) {
x2 = e.getX();
y2 = e.getY();
if(shapeName == null) return;
switch (shapeName){
case "實時直線":
RealLine.x1 = e.getX();
RealLine.y1 = e.getY();
RealLine.x2 = 0;
break;
}
}
第四步:實現重繪
到這裡,我們的畫圖板的雛形已經完成了,但是也存在以下幾個問題:
- ①當窗體發生變動(放大、窗體大小發生改變)時,已經繪製好的圖形就會消失.
- ②我們在使用實時直線的時候,繪製過程中會將其他圖形擦掉。
如何解決這些問題呢?
我們可以把每個的圖形看作一個類,再用List集合把它們儲存起來,然後重寫主頁面的paint方法(paint方法會在窗體初
始化、拖動、改變尺寸、移出螢幕、最小化、最大化時呼叫),將List中的圖形 在這個方法中遍歷繪製出來。
- 具體實現方法
@Override
public void paint(Graphics g){
super.paint(g);
for(Shapes shape : dl.shapeList){
shape.drawShape(g);
}
}
由於ArrayList只能存放一種物件,所以我們先建立一個父類shape,讓shape的子類去重寫drawShape方法。
在paint方法中遍歷ArrayList集合時,每個物件呼叫自己獨特的的drawShape方法,實現重繪。
- 我們將具有相同屬性的圖形定義為一個相同的類,例如直線、矩形、圓、謝爾賓斯基地毯、實心矩形、 實心圓、等腰三角形、
立方體、橡皮擦等圖形,只需要兩個點的座標,即可繪製成功,所以我們定義一個BasicShape類,然後重寫drawShape
方法來繪製它們 - shapes類(父類)
package drawBoard_test2;
import java.awt.Color;
import java.awt.Graphics;
public class Shapes {
public String shapeName; // 圖形的名稱(要根據圖形的名稱,判斷重繪的方法)
public Color color; //畫筆顏色(每個圖形都有自己的顏色,重繪的時候圖形的顏色也一樣要保留)
public void drawShape (Graphics g){
g.setColor(color);
}
}
- BasicShape類
package drawBoard_test2;
import javax.swing.*;
import java.awt.*;
public class BasicShape extends Shapes {
private int x1,y1,x2,y2;
public BasicShape(String shapeName, Color color,int x1, int y1, int x2, int y2) {
this.shapeName = shapeName;
this.color = color;
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
}
@Override
public void drawShape (Graphics g){
super.drawShape(g);
switch (shapeName){
case "直線":
g.setColor(color);
g.drawLine(x1,y1,x2,y2);
break;
case "矩形":
g.drawRect(Math.min(x1,x2),Math.min(y1,y2),Math.abs(x2-x1),Math.abs(y2-y1));
break;
case "圓" :
g.drawOval(Math.min(x1,x2),Math.min(y1,y2),Math.abs(x2-x1),Math.abs(y2-y1));
break;
case "謝爾賓斯基地毯" :
Sierpinski(g,Math.min(x1,x2),Math.min(y1,y2),Math.abs(x2-x1),Math.abs(y2-y1));
break;
case "實心矩形" :
g.fillRect(Math.min(x1,x2),Math.min(y1,y2),Math.abs(x2-x1),Math.abs(y2-y1));
break;
case "實心圓" :
g.fillOval(Math.min(x1,x2),Math.min(y1,y2),Math.abs(x2-x1),Math.abs(y2-y1));
break;
case "等腰三角形" :
g.drawLine(x1,y2,x2,y2);
g.drawLine(x1,y2,(x1+x2)/2,y1);
g.drawLine(x2,y2,(x1+x2)/2,y1);
break;
case "立方體" :
g.drawRect(Math.min(x1,x2),Math.min(y1,y2),Math.abs(x2-x1),Math.abs(y2-y1));
g.drawLine(x1+(int)((x2-x1)*1.414/4),y1-(int)((y2-y1)*1.414/4),x1,y1);
g.drawLine(x1+(int)((x2-x1)*1.414/4),y1-(int)((y2-y1)*1.414/4),x2+(int)((x2-x1)*1.414/4),y1-(int)((y2-y1)*1.414/4));
g.drawLine(x2,y1,x2+(int)((x2-x1)*1.414/4),y1-(int)((y2-y1)*1.414/4));
g.drawLine(x2+(int)((x2-x1)*1.414/4),y2-(int)((y2-y1)*1.414/4),x2+(int)((x2-x1)*1.414/4),y1-(int)((y2-y1)*1.414/4));
g.drawLine(x2+(int)((x2-x1)*1.414/4),y2-(int)((y2-y1)*1.414/4),x2,y2);
break;
case "橡皮擦" :
Color pre = g.getColor(); //記錄之前的顏色 ,用完再換回去
g.setColor( new JButton().getBackground());
g.fillRect(Math.min(x1,x2),Math.min(y1,y2),Math.abs(x2-x1),Math.abs(y2-y1));
g.setColor(pre);
break;
default:
break;
}
}
public void Sierpinski(Graphics g,int x,int y,int w,int h){
if(w>0&&h>0){
g.fillRect(x+w/3,y+h/3,w/3,h/3);
Sierpinski(g,x,y,w/3,h/3);
Sierpinski(g,x+w/3,y,w/3,h/3);
Sierpinski(g,x+2*w/3,y,w/3,h/3);
Sierpinski(g,x,y+h/3,w/3,h/3);
Sierpinski(g,x+2*w/3,y+h/3,w/3,h/3);
Sierpinski(g,x,y+2*h/3,w/3,h/3);
Sierpinski(g,x+w/3,y+2*h/3,w/3,h/3);
Sierpinski(g,x+2*w/3,y+2*h/3,w/3,h/3);
}
}
}
當繪製出一個圖形時,要將該圖形加入到List集合中,所以監聽器中的程式碼參考以下程式碼
@Override
public void mouseReleased(MouseEvent e) {
x3 = e.getX();
y3 = e.getY();
if(shapeName == null) return;
switch(shapeName){
case "直線":
case "矩形":
case "圓" :
case "謝爾賓斯基地毯":
case "實心矩形" :
case "實心圓" :
case "等腰三角形" :
case "立方體" :
case "橡皮擦" :
BasicShape basicShape = new BasicShape(shapeName, new Color(color.getRGB()), x2, y2, x3, y3);
basicShape.drawShape(g);
shapeList.add(basicShape);
break;
}
}
至此,我們就完成了簡單圖形的重繪。
我們還剩簽字筆、實時直線、遞迴KLine、三角形、多邊形、改進多邊形等圖形需要繪製。
這些圖形有什麼共同的屬性可以提取嗎?他們的共同點是座標點都比較多,數量不能確定,我們可以設定一個List屬性,把每個圖形的點
都存在這個集合裡, 然後重繪時,呼叫drawShape方法把集合裡的點取出來,再繪製出來。
說做就做,我們建立一個specialShape類,主要屬性為一個ArrayList集合,其餘屬性根據繪製的需要來定。
package drawBoard_test2;
import java.awt.*;
import java.util.ArrayList;
public class SpecialShape extends Shapes {
public ArrayList<Point> specialList = new ArrayList<>();
private Point first;
private Point pre;
private Point cur;
public SpecialShape(String shapeName, Color color, ArrayList<Point> specialList) {
this.shapeName = shapeName;
this.color = color;
for (Point p : specialList) {
this.specialList.add(p);
}
}
@Override
public void drawShape(Graphics g) {
super.drawShape(g);
switch (shapeName) {
case "三角形":
case "多邊形":
case "改進多邊形":
if (specialList.isEmpty()) break;
int i = 0;
first = specialList.get(i++);
cur = first;
while (i < specialList.size()) {
pre = cur;
cur = specialList.get(i++);
g.drawLine(pre.x, pre.y, cur.x, cur.y);
}
g.drawLine(first.x, first.y, cur.x, cur.y);
break;
case "簽字筆":
case "遞迴KLine":
case "實時直線":
if (specialList.isEmpty()) break;
int j = 0;
while (j < specialList.size()-1) {
g.drawLine(specialList.get(j).x, specialList.get(j).y, specialList.get(j+1).x, specialList.get(++j).y);
}
break;
}
}
}
接下來,我們需要做的就是將每個圖形的點按順序新增進specialList中,點都收集完之後,將一個新建的specialShape物件放入我們的圖形集合ShapeList中,所以我們修改每個圖形中的程式碼:
- 三角形類的程式碼參考:
/**
* @param specialList 三角形的頂點存入SpecialShape的集合,存入的順序應該為順次連線的點的順序
* @param shapeList 重繪時使用的圖形集合
*/
public void drawTriangle(MouseEvent e , Graphics g, ArrayList<Point> specialList, Color color, ArrayList<Shapes> shapeList){
if(num == 0){
specialList.clear();
x1 = e.getX();
y1 = e.getY();
num++;
specialList.add(new Point(x1,y1));
}else if(num == 1){
x2 = e.getX();
y2 = e.getY();
g.drawLine(x1,y1,x2,y2);
num++;
specialList.add(new Point(x2,y2));
}else if(num == 2){
x3 = e.getX();
y3 = e.getY();
g.drawLine(x3,y3,x2,y2);
g.drawLine(x3,y3,x1,y1);
num=0;
specialList.add(new Point(x3,y3));
SpecialShape specialShape = new SpecialShape("三角形", new Color(color.getRGB()), specialList);
shapeList.add(specialShape);
}
}
相應的監聽器中的程式碼,做出相應的修改,
/**
* 建立一個specialList集合用來存放每個圖形的點,將它傳入圖形的繪製方法中,
* 當收集到所有的點時,將以集合作為成員變數建立的specialShape物件存入shape集合中。
*/
ArrayList<Point> specialList = new ArrayList<>();
case "三角形" :
new Triangle().drawTriangle(e,g,specialList,color,shapeList);
break;
其他的類的方法也是如出一轍,大家在寫出來之後,可以和鄙人的程式碼進行比對。這裡給出其餘程式碼:
- 多邊形
public class Polygon {
static int x1,y1,x2,y2,x3,y3;
static int num;
/**
*
* @param e
* @param g
* @param specialList 多邊形的頂點存入SpecialShape的集合,存入的順序應該為順次連線的點的順序
* @param color
* @param shapeList 重繪時使用的圖形集合
*/
public void drawPolygon(MouseEvent e , Graphics g, ArrayList<Point> specialList, Color color, ArrayList<Shapes> shapeList){
if(num == 0){ //第一個點
x1 = e.getX();
y1 = e.getY();
num++;
specialList.clear();
specialList.add(new Point(x1,y1));
}else if(num == 1){ //
x2 = e.getX();
y2 = e.getY();
g.drawLine(x1,y1,x2,y2);
num++;
specialList.add(new Point(x2,y2));
}else if (num == 2){
if(e.getButton()==3){ //右鍵結束時,所有的點已經確定,我們新建一個specialShape物件存入specialList集合中。
g.drawLine(x1,y1,x2,y2);
num=0;
SpecialShape specialShape = new SpecialShape("多邊形", new Color(color.getRGB()), specialList);
shapeList.add(specialShape);
specialList.clear();
return;
}
x3 = e.getX();
y3 = e.getY();
g.drawLine(x3,y3,x2,y2);
specialList.add(new Point(x3,y3));
num++;
}else if(num == 3){
if(e.getButton()==3){
g.drawLine(x1,y1,x3,y3);
num=0;
SpecialShape specialShape = new SpecialShape("多邊形", new Color(color.getRGB()), specialList);
shapeList.add(specialShape);
specialList.clear();
return;
}
x2 = e.getX();
y2 = e.getY();
g.drawLine(x3,y3,x2,y2);
specialList.add(new Point(x2,y2));
num--;
}
}
}
/**
* 多邊形對應監聽器中的方法
* mouseClicked方法
*/
case "多邊形":
new Polygon().drawPolygon(e,g,specialList,color,shapeList);
break;
- 改進多邊形
/**
* 改進多邊形類的draw方法
* @param ArrayList<Point> list 多邊形頂點的集合,順序為滑鼠繪製時 點選的順序
* @param ArrayList<Point> specialList 多邊形的頂點存入SpecialShape的集合,存入的順序應該為順次連線的點的順序
* @param ArrayList<Shapes> shapeList 重繪時使用的圖形集合
*/
public void drawPolygonPro(ArrayList<Point> list, Graphics g, ArrayList<Point> specialList, Color color, ArrayList<Shapes> shapeList){
if(list.size() == 0||list.size() == 1||list.size() == 2) return;
int right = findRight(list);
System.out.println(right);
Point rightPoint = new Point(list.get(right).x, list.get(right).y);
specialList.add(rightPoint);
list.remove(right);
double[] tan = new double[list.size()];
for (int i = 0; i < list.size(); i++) {
tan[i] = ((double) rightPoint.y-list.get(i).y)/(rightPoint.x-list.get(i).x);
}
int pre;
int cur = indexOfMax(tan);
specialList.add(list.get(cur));
g.drawLine(list.get(cur).x,list.get(cur).y, rightPoint.x, rightPoint.y);
tan[cur] = Integer.MIN_VALUE;
for (int i = 0; i < tan.length-1; i++) {
pre = cur;
cur = indexOfMax(tan);
specialList.add(list.get(cur));
g.drawLine(list.get(pre).x,list.get(pre).y, list.get(cur).x,list.get(cur).y);
tan[cur] = Integer.MIN_VALUE;
}
g.drawLine(list.get(cur).x,list.get(cur).y, rightPoint.x, rightPoint.y);
}
/**
* 改進多邊形對應監聽器中的方法
*/
case "改進多邊形":
if(e.getButton()==3){
new PolygonPro().drawPolygonPro(list,g,specialList,color,shapeList);
SpecialShape specialShape = new SpecialShape("改進多邊形",color,specialList);
shapeList.add(specialShape);
specialList.clear();
list.clear();
break;
}else{
Point point = new Point(e.getX(),e.getY());
list.add(point);
break;
}
- 遞迴KLine曲線
case "遞迴KLine":
KLine(x2,y2,x3,y3,Math.abs(y3-y2));
specialList.add(new Point(x3,y3));
SpecialShape specialShape = new SpecialShape(shapeName, new Color(color.getRGB()), specialList);
shapeList.add(specialShape);
specialList.clear();
break;
/**
* 遞迴Kline實現方法
*/
public void KLine(int x1 , int y1 , int x2 , int y2, int x){
if(Math.abs(x2-x1)<= 1 || Math.abs(y2-y1) <= 1 || x < 1){
g.drawLine(x1, y1, x2, y2);
specialList.add(new Point(x1,y1));
return;
}
Random random = new Random(0);
int ran = random.nextInt(x);
int mid = ((y2+y1)/2-x+ran*2);
x = (int)(x*0.618);
KLine(x1, y1, (x1+x2)/2, mid,x);
KLine((x1+x2)/2, mid, x2,y2,x);
}
- 簽字筆類
/**
* 簽字筆類的程式碼修改
*/
public void draw(MouseEvent e , Graphics g, ArrayList<Point> specialList, Color color, ArrayList<Shapes> shapeList) {
switch(state){
case 1 :
x1 = e.getX();
y1 = e.getY();
specialList.add(new Point(x1,y1));
state = 2;
break;
case 2 :
x2 = e.getX();
y2 = e.getY();
specialList.add(new Point(x2,y2));
g.drawLine(x2,y2,x1,y1);
state = 3;
break;
case 3 :
x1 = e.getX();
y1 = e.getY();
specialList.add(new Point(x1,y1));
g.drawLine(x2,y2,x1,y1);
state = 2;
break;
}
}
/**
* mouseDragged
*/
case "簽字筆":
new Pen().draw(e,g,specialList,color,shapeList);
break;
/**
* mouseReleased
*/
case "簽字筆" :
SpecialShape specialShape2 = new SpecialShape(shapeName, new Color(color.getRGB()), specialList);
shapeList.add(specialShape2);
specialList.clear();
Pen.state=1;
break;
- 實時直線類
public class RealLine {
public static int x1,y1,x2,y2,x3,y3;
public void draw(MouseEvent e , Graphics g, ArrayList<Point> specialList, Color color, ArrayList<Shapes> shapeList){
Color pre = g.getColor();
g.setColor( new JButton().getBackground());
if(x2 !=0 ){
g.drawLine(x2,y2,x1,y1);
}
g.setColor(pre);
x3 = e.getX();
y3 = e.getY();
g.drawLine(x3,y3,x1,y1);
x2=x3;
y2=y3;
}
}
/**
* mousePressed
*/
case "實時直線":
specialList.add(new Point(x2,y2));
RealLine.x1 = e.getX();
RealLine.y1 = e.getY();
RealLine.x2 = 0;
break;
/**
* mouseReleased
*/
case "實時直線":
specialList.add(new Point(x3,y3));
SpecialShape specialShape3 = new SpecialShape(shapeName, new Color(color.getRGB()), specialList);
shapeList.add(specialShape3);
specialList.clear();
break;
接下來,我們發現,圖形確實可以實現重繪了,但是每次呼叫paint方法時,繪製的速度總是很慢,尤其是重繪謝爾賓斯基地毯時,是肉眼可見的慢,這是什麼原因導致的呢?
我們知道,繪製的內容要顯示到螢幕上,需要把 記憶體資料 提交 給顯示卡 ,通過顯示卡再渲染計算 顯示到螢幕。
計算機的計算速度是非常快的,但是我們每計算出幾個畫素點,就直接輸出到螢幕上,以至於 要畫的次數很多,這導致了計算機IO 與 計算不匹配。
我們如何解決這種問題?
計算快,但IO很慢,我們就讓計算機先計算好,再輸出到螢幕上。
我們使用 快取(BufferedImage類),把下一幀需要顯示的畫面上所有的圖形內容都計算好並存起來,然後再一次性繪出 。
BufferedImage 快取圖片 屬性:寬、高 格式為畫素儲存格式 使用Graphics類作為畫筆
- 來看迭代後的paint的程式碼實現
public void paint(Graphics g){
super.paint(g);
BufferedImage bufferedImage = new BufferedImage(1000,800,BufferedImage.TYPE_INT_ARGB);
Graphics buffg = bufferedImage.getGraphics();
for(Shapes shape : dl.shapeList){
shape.drawShape(buffg);
}
g.drawImage(bufferedImage,0,0,null);
}
此時再來試試重繪的功能,是不是感覺很神奇。
- 我們還有一個未解決的問題,就是實時直線拖動時會擦掉畫板上其他圖形,這如何解決呢?
- 解決方法:在實時直線的繪製過程中,不斷地進行重繪,把被擦掉的畫素點補回來。
public class RealLine {
public static int x1,y1,x2,y2,x3,y3;
public void draw(MouseEvent e , Graphics g, ArrayList<Point> specialList, Color color, ArrayList<Shapes> shapeList){
Color pre = g.getColor();
g.setColor( new JButton().getBackground());
if(x2 !=0 ){
g.drawLine(x2,y2,x1,y1);
}
g.setColor(pre);
x3 = e.getX();
y3 = e.getY();
g.drawLine(x3,y3,x1,y1);
x2=x3;
y2=y3;
BufferedImage bufferedImage = new BufferedImage(800,800,BufferedImage.TYPE_INT_ARGB);
Graphics buffs = bufferedImage.getGraphics();
for(Shapes shape : shapeList){
shape.drawShape(buffs);
}
g.drawImage(bufferedImage,0,0,null);
}
}
第五步:實現撤回,清空功能
- 我們已經實現了重繪功能,撤回就很簡單了,我們只需要把shapeList中最近新增進去的圖形刪掉,然後重繪就可以了。
- 清空就是把shapeList中所有的圖形刪掉,然後重繪。
- 程式碼實現:
/**
* actionPerformed
*/
switch(shapeName) {
case "撤回":
if (!shapeList.isEmpty()) {
shapeList.remove(shapeList.size() - 1);
drawUI.paint(g);
}
break;
case "清空" :
shapeList.clear();
drawJPanel.paint(g);
break;
}
/**
* 然後我們需要涉及到傳值的問題,我們在監聽器頁面新增一個drawUI物件成員,然後把DrawUI類中的main函式中的drawUI物件傳給監聽器
*/
第六步:開啟與儲存操作
- 兩點需要注意:
- ①為了加快開啟圖片的速度,我們把圖片需要顯示的畫面都畫在BufferedImage中,然後再一次性繪出。
- ②我們繪製的圖形可以實現撤回功能,那麼我們開啟的圖片能不能也實現撤回功能呢?
- 當然可以,我們只需要把開啟的圖片也存入ShapeList集合中,所以我們建立一個ImageShape類(繼承Shape類),用來儲存圖片。
- ImageShape類
package drawBoard_test2;
import java.awt.*;
import java.awt.image.BufferedImage;
public class ImageShape extends Shapes {
BufferedImage bufferedImage;
@Override //重繪方法
public void drawShape(Graphics g){
g.drawImage(bufferedImage,0,0,null);
}
//封裝 BufferedImage的set方法
public void setBufferedImage(BufferedImage bufferedImage) {
this.bufferedImage = bufferedImage;
}
}
- 開啟
String fileName;
/**
* 開啟操作步驟:將圖片轉化為二維陣列,遍歷每個點在畫圖板上畫出
* JFileChooser 檔案選擇器
* FileNameExtensionFilter 檔案過濾器,構造方法的引數JPG & GIF Images為篩選檔案的選項, "jpg", "gif"為篩選檔案的型別
*
*/
//actionPerformed
case "開啟" :
JFileChooser chooser = new JFileChooser();
FileNameExtensionFilter filter = new FileNameExtensionFilter (
"JPG & GIF Images", "jpg", "gif");
chooser.setFileFilter(filter);
int returnVal = chooser.showOpenDialog(null);
if(returnVal == JFileChooser.APPROVE_OPTION) { //JFileChooser.APPROVE_OPTION 批准選項
System.out.println("You chose to open this file: " +
chooser.getSelectedFile().getPath());
fileName = chooser.getSelectedFile().getPath(); //獲取檔案的本地路徑
}
BufferedImage bufferedImage = new BufferedImage(800,800,BufferedImage.TYPE_INT_ARGB);
Graphics buffg = bufferedImage.getGraphics();
int[][] img = getImagePixel(fileName);
drawImage(buffg,img);
ImageShape imageShape = new ImageShape();
imageShape.setBufferedImage(bufferedImage);
g.drawImage(bufferedImage,0,0,null);
shapeList.add(imageShape);
break;
/**
* drawImage將圖形畫在畫圖板上
*/
public void drawImage(Graphics g ,int[][] img){
for (int i = 0; i < img.length; i++) {
for (int j = 0; j < img[i].length; j++) {
Color c = new Color(img[i][j]);
g.setColor(c);
g.drawOval(i , j, 1, 1);
}
}
}
/**
* getImagePixel 返回圖片的二維陣列
*/
public static int[][] getImagePixel(String filePath) {
File file = new File(filePath); //filePath為檔案路徑
BufferedImage bi = null;
try{
bi = ImageIO.read(file);
} catch (Exception e) {
e.printStackTrace();
}
int w = bi.getWidth();
int h = bi.getHeight();
int[][] imIndex = new int[w][h];
for (int i = 0; i < w; i++) {
for (int j = 0; j < h; j++) {
int pixel = bi.getRGB(i,j);
imIndex[i][j] = pixel;
}
}
return imIndex;
}
}
return imIndex;
}
- 儲存
/**
* 儲存為的檔名的字尾應為png
*/
case "儲存":
JFileChooser chooser2 = new JFileChooser();
FileNameExtensionFilter filter2 = new FileNameExtensionFilter(
"JPG & GIF Images", "jpg","gif"
);
chooser2.setFileFilter(filter2);
int returnVal2 = chooser2.showSaveDialog(null);
if(returnVal2 == JFileChooser.APPROVE_OPTION){
System.out.println("You choose to save this file:" +
chooser2.getSelectedFile().getPath());
}
//把所有的圖形重繪到bufferedImage上,再把bufferedImage存入圖片檔案中
BufferedImage bufferedImage2 = new BufferedImage(800,800,BufferedImage.TYPE_INT_ARGB);
Graphics buffg2 = bufferedImage2.getGraphics();
for(Shape shape : shapeList ){
shape.drawShape(buffg2);
}
File file2 = new File(chooser2.getSelectedFile().getPath());
try {
ImageIO.write(bufferedImage2,"png",file2);
} catch (IOException ex) {
ex.printStackTrace();
}
break;
第七步:美化介面,並新增圖片處理功能按鈕
此時我們的畫布、圖形按鈕、顏色按鈕放在一起,如果我們後面再加入圖形處理按鈕,介面將會變得很不整潔,
所以我們使用邊框佈局來將窗體分割槽管理。
我們要將畫板從整個窗體改成了一個JPanel,但是我們的重繪功能還需要重寫過的paint方法,所以我們新建一個DrawJPanel類
來繼承JPanel類,去重寫paint方法。
- 介面效果:
public class DrawUI extends JFrame {
DrawListener dl = new DrawListener();
String[] strs = {"直線","簽字筆","實時直線", "謝爾賓斯基地毯","遞迴KLine","矩形", "圓", "實心矩形", "實心圓", "等腰三角形", "三角形", "多邊形",
"改進多邊形","立方體", "橡皮擦", "撤回", "儲存", "開啟"};
Color[] color = {Color.red,Color.yellow,Color.black,Color.blue};
public void addShapeButton(JComponent component){
for(String str : strs){
JButton btn = new JButton(str);
btn.addActionListener(dl);
component.add(btn);
}
}
public void addColorButton(JComponent component){
Dimension dim = new Dimension(30,30);
for(Color c : color){
JButton btn = new JButton();
btn.setBackground(c);
btn.setPreferredSize(dim);
btn.addActionListener(dl);
component.add(btn);
}
Dimension dim2 = new Dimension(95,30);
JButton btn = new JButton("選擇顏色...");
btn.setPreferredSize(dim2);
btn.addActionListener(dl);
component.add(btn);
}
public void addBeautyButton(JComponent component){
String[] str = {"原圖","馬賽克","灰度","二值化","背景替換","油畫","圖片融合","磨皮"};
for(String s : str){
JButton btn = new JButton(s);
btn.addActionListener(dl);
component.add(btn);
}
}
public void initUI(){
JFrame jf = new JFrame("畫圖板");
jf.setTitle("畫圖板");
jf.setLayout(new BorderLayout());
jf.setSize(1000,800);
jf.setLocationRelativeTo(null);
jf.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
//選單欄
JMenuBar jMenuBar = new JMenuBar();
JMenu jMenu = new JMenu("選單",true);
String[] Menu = {"撤回","開啟","儲存","清空"};
for(String s : Menu){
JMenuItem jMenuItem = new JMenuItem(s);
jMenu.add(jMenuItem);
jMenuItem.addActionListener(dl);
}
jMenuBar.add(jMenu);
jf.setJMenuBar(jMenuBar);
JPanel shapeChooserPanel = new JPanel();
DrawJPanel drawPanel = new DrawJPanel();
JPanel ChooserPanel = new JPanel();
JPanel ColorChooserPanel = new JPanel();
ChooserPanel.setLayout(new BorderLayout());
dl.drawJPanel = drawPanel;
JPanel RightPanel = new JPanel();
//大小
Dimension dim = new Dimension(150,80);
shapeChooserPanel.setPreferredSize(dim);
ChooserPanel.setPreferredSize(dim);
Dimension dim2 = new Dimension(150,330);
RightPanel.setPreferredSize(dim2);
ColorChooserPanel.setPreferredSize(dim2);
ChooserPanel.setPreferredSize(dim2);
//背景顏色
Color color1 = new Color(-3355444);
shapeChooserPanel.setBackground(color1);
Color color2 = new Color(-6710887);
ColorChooserPanel.setBackground(color2);
ChooserPanel.setBackground(color2);
RightPanel.setBackground(color1);
//方位
jf.add(shapeChooserPanel,BorderLayout.NORTH);
jf.add(ChooserPanel,BorderLayout.EAST);
jf.add(drawPanel,BorderLayout.CENTER);
ChooserPanel.add(RightPanel,BorderLayout.SOUTH);
ChooserPanel.add(ColorChooserPanel,BorderLayout.NORTH);
//新增按鈕
addShapeButton(shapeChooserPanel);
addColorButton(ColorChooserPanel);
addBeautyButton(RightPanel);
jf.setVisible(true);
Graphics g = drawPanel.getGraphics ();
drawPanel.addMouseMotionListener(dl);
drawPanel.addMouseListener(dl);
drawPanel.setDl(dl);
dl.setG(g);
}
public static void main(String[] args) {
new DrawUI().initUI();
}
}
- 選擇顏色
/**
* 監聽器中的actionPerformed方法
*/
if(btn_action.equals("選擇顏色...")){
color = JColorChooser.showDialog(drawJPanel, "選擇顏色", Color.red);
System.out.println(color.getRGB());
g.setColor(color);
return;
}
第八步:影像處理功能
深入理解color類:
- rgb數字構成顏色 Color c = new Color(200,50,100);其值在0~255之間。
- rgb的三個數字分別對應red,green,blue
- int數字構成顏色 Color c = new Color(-3355444),其值為int型別。
- 馬賽克
/**
* 馬賽克
* 把畫素點放大
*/
case "馬賽克":
BufferedImage bufferedImage3 = new BufferedImage(800,800,BufferedImage.TYPE_INT_ARGB);
Graphics buffg3 = bufferedImage3.getGraphics();
int[][] img3 = getImagePixel(fileName);
drawImage_MSK(buffg3,img3);
ImageShape imageShape3 = new ImageShape();
imageShape3.setBufferedImage(bufferedImage3);
g.drawImage(bufferedImage3,0,0,null);
shapeList.add(imageShape3);
break;
public void drawImage_MSK(Graphics g ,int[][] img){
int w = (drawJPanel.getWidth()- img.length)/2;
int h = (drawJPanel.getHeight()- img[0].length)/2;
for (int i = 0; i < img.length; i+=8) {
for (int j = 0; j < img[i].length; j+=8) {
Color c = new Color(img[i][j]);
g.setColor(c);
g.fillRect(i+w , j+h, 8, 8);
}
}
}
- 灰度
/**
* 灰度影像
* rgb三個分量都相同,一般可以取其平均值
* 這裡使用的是灰度值的浮點法計算,讀者可以參考該網址,嘗試一下Gamma校正演算法
* https://baike.baidu.com/item/%E7%81%B0%E5%BA%A6%E5%80%BC/10259111?fr=aladdin
*/
case "灰度":
BufferedImage bufferedImage6 = new BufferedImage(800,800,BufferedImage.TYPE_INT_ARGB);
Graphics buffg6 = bufferedImage6.getGraphics();
int[][] img6 = getImagePixel(fileName);
drawImage_gray(buffg6,img6);
ImageShape imageShape6 = new ImageShape();
imageShape6.setBufferedImage(bufferedImage6);
g.drawImage(bufferedImage6,0,0,null);
shapeList.add(imageShape6);
break;
public void drawImage_gray(Graphics g ,int[][] img){
int w = (drawJPanel.getWidth()- img.length)/2;
int h = (drawJPanel.getHeight()- img[0].length)/2;
for (int i = 0; i < img.length; i++) {
for (int j = 0; j < img[i].length; j++) {
int value = img[i][j];
int red = (value>>16) & 0xff;
int green = (value>>8) & 0xff;
int blue = value & 0xff;
int gray = (int) (0.3 * red + 0.59 * green + 0.11 * blue);
Color c = new Color(gray,gray,gray);
g.setColor(c);
g.fillRect(i+w , j+h, 1, 1);
}
}
}
- 二值化
/**
* 二值影像
* 指僅有黑白兩色的影像(大於某值的畫白,小於某值的畫黑)
*/
case "二值化":
BufferedImage bufferedImage7 = new BufferedImage(800,800,BufferedImage.TYPE_INT_ARGB);
Graphics buffg7 = bufferedImage7.getGraphics();
int[][] img7 = getImagePixel(fileName);
drawImage_binary(buffg7,img7);
ImageShape imageShape7 = new ImageShape();
imageShape7.setBufferedImage(bufferedImage7);
g.drawImage(bufferedImage7,0,0,null);
shapeList.add(imageShape7);
break;
public void drawImage_binary(Graphics g ,int[][] img){
int w = (drawJPanel.getWidth()- img.length)/2;
int h = (drawJPanel.getHeight()- img[0].length)/2;
for (int i = 0; i < img.length; i++) {
for (int j = 0; j < img[i].length; j++) {
int value = img[i][j];
int red = (value>>16) & 0xff;
int green = (value>>8) & 0xff;
int blue = value & 0xff;
int gray = (int) (0.3 * red + 0.59 * green + 0.11 * blue);
if(gray < 150){
g.setColor(Color.black);
}else {
g.setColor(Color.white);
}
g.fillRect(i+w , j+h, 1, 1);
}
}
}
- 背景替換
/**
* 背景替換影像
* 當圖片的背景為白色時,我們將大於某一值的畫素點,替換為另一張圖片的畫素點
*/
case "背景替換":
BufferedImage bufferedImage8 = new BufferedImage(800,800,BufferedImage.TYPE_INT_ARGB);
Graphics buffg8 = bufferedImage8.getGraphics();
int[][] img8 = getImagePixel(fileName);
int[][] background = getImagePixel("C:\\Users\\13630\\Desktop\\背景.jpg");
drawImage_replaceBackground(buffg8,img8,background);
ImageShape imageShape8 = new ImageShape();
imageShape8.setBufferedImage(bufferedImage8);
g.drawImage(bufferedImage8,0,0,null);
shapeList.add(imageShape8);
break;
public void drawImage_replaceBackground(Graphics g ,int[][] img,int[][] background){
int w = (drawJPanel.getWidth()- img.length)/2;
int h = (drawJPanel.getHeight()- img[0].length)/2;
for (int i = 0; i < img.length; i++) {
for (int j = 0; j < img[i].length; j++) {
int value = img[i][j];
int red = (value>>16) & 0xff;
int green = (value>>8) & 0xff;
int blue = value & 0xff;
int gray = (int) (0.3 * red + 0.59 * green + 0.11 * blue);
if(gray > 240&&i< background.length&&j<background[i].length){
g.setColor(new Color(background[i][j]));
}else {
g.setColor(new Color(img[i][j]));
}
g.fillRect(i+w , j+h, 1, 1);
}
}
}
- 油畫
/**
* 原理與馬賽克類似,不同的是油畫效果要填充隨機大小的色塊
*/
case "油畫":
BufferedImage bufferedImage9 = new BufferedImage(800,800,BufferedImage.TYPE_INT_ARGB);
Graphics buffg9 = bufferedImage9.getGraphics();
int[][] img9 = getImagePixel(fileName);
drawImage_OilPainting(buffg9,img9);
ImageShape imageShape9 = new ImageShape();
imageShape9.setBufferedImage(bufferedImage9);
g.drawImage(bufferedImage9,0,0,null);
shapeList.add(imageShape9);
break;
public void drawImage_OilPainting(Graphics g ,int[][] img){
int w = (drawJPanel.getWidth()- img.length)/2;
int h = (drawJPanel.getHeight()- img[0].length)/2;
for (int i = 0; i < img.length; i+=5) {
for (int j = 0; j < img[i].length; j+=5) {
g.setColor(new Color(img[i][j]));
Random random = new Random();
int ran = random.nextInt(20)+5;
g.fillOval(i+w , j+h, ran, ran);
}
}
}
- 圖片融合
/**
* 需要兩張照片
* 融合後圖片畫素點的顏色 為融合前的兩張照片畫素點顏色以不同比例融合
*/
case "圖片融合":
BufferedImage bufferedImage10 = new BufferedImage(800,800,BufferedImage.TYPE_INT_ARGB);
Graphics buffg10 = bufferedImage10.getGraphics();
int[][] img10 = getImagePixel(fileName);
int[][] background2 = getImagePixel("C:\\Users\\13630\\Desktop\\背景.jpg");
drawImage_fusion(buffg10,img10,background2);
ImageShape imageShape10 = new ImageShape();
imageShape10.setBufferedImage(bufferedImage10);
g.drawImage(bufferedImage10,0,0,null);
shapeList.add(imageShape10);
break;
public void drawImage_fusion(Graphics g ,int[][] img,int[][] background){
int w = Math.min(img.length, background.length);
int h = Math.min(img[0].length, background[0].length);
for (int i = 0; i < w; i++) {
for (int j = 0; j < h; j++) {
Color ca = new Color(img[i][j]);
Color cb = new Color(background[i][j]);
int red = (int) (ca.getRed()*0.7+ cb.getRed()*0.3);
int green = (int)(ca.getGreen()* 0.3+cb.getGreen()*0.7);
int blue = (int)(ca.getBlue()*0.3+ cb.getBlue()*0.7);
Color c = new Color(red,green,blue);
g.setColor(c);
g.fillRect(i , j, 1, 1);
}
}
}
- 原圖
case "原圖":
BufferedImage bufferedImage5 = new BufferedImage(800,800,BufferedImage.TYPE_INT_ARGB);
Graphics buffg5 = bufferedImage5.getGraphics();
int[][] img5 = getImagePixel(fileName);
drawImage(buffg5,img5);
ImageShape imageShape5 = new ImageShape();
imageShape5.setBufferedImage(bufferedImage5);
g.drawImage(bufferedImage5,0,0,null);
shapeList.add(imageShape5);
break;
//畫在畫圖區域的中央
public void drawImage(Graphics g ,int[][] img){
int w = (drawPanel.getWidth()- img.length)/2;
int h = (drawPanel.getHeight()- img[0].length)/2;
for (int i = 0; i < img.length; i++) {
for (int j = 0; j < img[i].length; j++) {
Color c = new Color(img[i][j]);
g.setColor(c);
g.drawOval(w+i , h+j, 1, 1);
}
}
}
- 磨皮
磨皮是為了把有瑕疵的地方覆蓋住,所以我們用一種和周圍相同顏色的粗畫筆去覆蓋圖片上的瑕疵。 - 我們實時獲取滑鼠所在位置的顏色,然後畫出與此顏色相同的顏色,實現方式與簽字筆相同
- 磨皮類
package drawBoard_test2;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
public class SkinGrinding {
public static int x1,y1,x2,y2;
public static int state = 1;
public void draw(MouseEvent e , Graphics2D g, ArrayList<Point> specialList, int[][] img, ArrayList<Shapes> shapeList,int w,int h) {
switch(state){
case 1 :
x1 = e.getX();
y1 = e.getY();
specialList.add(new Point(x1,y1));
state = 2;
break;
case 2 :
x2 = e.getX();
y2 = e.getY();
g.setColor(new Color(img[x2-w][y2-h]));
specialList.add(new Point(x2,y2));
g.drawLine(x2,y2,x1,y1);
state = 3;
break;
case 3 :
x1 = e.getX();
y1 = e.getY();
specialList.add(new Point(x1,y1));
g.setColor(new Color(img[x1-w][y1-h]));
g.drawLine(x2,y2,x1,y1);
state = 2;
break;
}
}
}
監聽器中新增的程式碼
監聽器中加一個img11[][],用來存放當然處理的照片的畫素點
/**
* actionPerformed
*/
case "磨皮":
img11 = getImagePixel(fileName);
break;
/**
* mousePressed
*/
case "磨皮":
g2D = (Graphics2D)g;
g2D.setStroke (new BasicStroke (3));
specialList.add(new Point(x2,y2));
break;
/**
* mouseReleased
*/
case "磨皮":
SpecialShape specialShape4 = new SpecialShape(shapeName, new Color(color.getRGB()), specialList);
shapeList.add(specialShape4);
specialList.clear();
SkinGrinding.state=1;
break;
/**
* mouseDragged
*/
case "磨皮":
int w = (drawJPanel.getWidth()- img11.length)/2;
int h = (drawJPanel.getHeight()- img11[0].length)/2;
new SkinGrinding().draw(e,g2D,specialList,img11,shapeList,w,h);
break;
第九步:”更多操作“介面的繪製
- 先看效果圖:
22.cnblogs.com/blog/2555328/202204/2555328-20220414151122093-1753505041.png)
package drawBoard_test2;
import javax.swing.*;
import java.awt.*;
import java.util.ArrayList;
public class ButtonUI extends JFrame {
public static DrawUI drawUI;
public void init (){
JFrame jf = new JFrame();
jf.setTitle("更多操作");
jf.setSize(380,500);
jf.setLocationRelativeTo(drawUI);
jf.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
jf.setLayout(new FlowLayout());
addJSlider(jf);
addButton(jf);
addJSlider2(jf);
jf.setVisible(true);
}
public void addButton (JFrame component){
String[] strings = {"放大130%","縮小50%","向左旋轉","向右旋轉"};
for(String s : strings){
JButton btn = new JButton(s);
component.add(btn);
btn.addActionListener(DrawUI.dl);
}
}
public void addJSlider(JFrame component){
JLabel jl = new JLabel("縮放比例(%):");
JSlider jSlider = new JSlider(0,200);
jSlider.setToolTipText("縮放比例");
jSlider.setMajorTickSpacing(30);
jSlider.setMinorTickSpacing(10);
jSlider.setPaintLabels(true);
jSlider.setPaintTicks(true);
jSlider.addChangeListener(DrawUI.dl);
component.add(jl);
component.add(jSlider);
}
public void addJSlider2(JFrame component){
JLabel jl1 = new JLabel("紅色亮度(%):");
JSlider jSlider1 = new JSlider(0,0,200,100);
jSlider1.setToolTipText("紅色");
jSlider1.setMajorTickSpacing(30);
jSlider1.setMinorTickSpacing(10);
jSlider1.setPaintLabels(true);
jSlider1.setPaintTicks(true);
jSlider1.addChangeListener(DrawUI.dl);
component.add(jl1);
component.add(jSlider1);
JLabel jl2 = new JLabel("綠色亮度(%):");
JSlider jSlider2 = new JSlider(0,0,200,100);
jSlider2.setToolTipText("綠色");
jSlider2.setMajorTickSpacing(30);
jSlider2.setMinorTickSpacing(10);
jSlider2.setPaintLabels(true);
jSlider2.setPaintTicks(true);
jSlider2.addChangeListener(DrawUI.dl);
component.add(jl2);
component.add(jSlider2);
JLabel jl3 = new JLabel("藍色亮度(%):");
JSlider jSlider3 = new JSlider(0,0,200,100);
jSlider3.setToolTipText("藍色");
jSlider3.setMajorTickSpacing(30);
jSlider3.setMinorTickSpacing(10);
jSlider3.setPaintLabels(true);
jSlider3.setPaintTicks(true);
jSlider3.addChangeListener(DrawUI.dl);
component.add(jl3);
component.add(jSlider3);
//確認和取消按鈕;
JButton btn1 = new JButton("確認");
btn1.addActionListener(DrawUI.dl);
component.add(btn1);
JButton btn2 = new JButton("取消");
btn2.addActionListener(DrawUI.dl);
component.add(btn2);
}
public static void main(String[] args) {
new ButtonUI().init();
}
}
第十步:放大、縮小功能
- 放大縮小的方法:
- 獲取原圖形畫素點的二維陣列,用最鄰近元法計算出待求畫素點,再利用BufferedImage作為緩衝,畫到畫布上。
- 最鄰近元法參考這個網站:影像插值_百度百科
@Override
public void stateChanged(ChangeEvent e) {
JSlider jSlider = (JSlider)e.getSource();
String s = jSlider.getToolTipText();
switch (s){
case "縮放比例":
multiple = jSlider.getValue();
int[][] img = getImagePixel(fileName);
BufferedImage bufferedImage = new BufferedImage(800,800,BufferedImage.TYPE_INT_ARGB);
Graphics buffg = bufferedImage.getGraphics();
drawImage_multiple(buffg,img);
g.drawImage(bufferedImage,0,0,null);
break;
}
}
public void drawImage_multiple(Graphics g , int[][] img){
int w = (int)((drawJPanel.getWidth()- img.length*1.0*(multiple)/100)/2);
int h = (int)((drawJPanel.getHeight()- img[0].length*1.0*multiple/100)/2);
for (int i = 0; i < img.length; i++) {
for (int j = 0; j < img[i].length; j++) {
g.setColor(new Color(img[i][j]));
for (int k = (int)(i*1.0*multiple/100); k < (int)((i+1)*1.0*multiple/100) ; k++) {
for (int l = (int)(1.0*j*multiple/100); l < (int)((j+1)*1.0*multiple/100); l++) {
g.drawRect(k+w,l+h,1,1);
}
}
}
}
}
第十一步:圖片的顏色調整
- 要實現的功能:通過滑動條,分別用來改變紅綠藍三種顏色的數值大小,來達到調整整個圖片顏色的效果
- 實現途徑:自己編寫一個儲存圖片的動態陣列類,將red,green,blue分別用一個矩陣陣列儲存起來,
package drawBoard_test2;
import javax.swing.text.Segment;
import java.awt.image.BufferedImage;
/**
* 這是一個用來儲存圖片的動態陣列類 /可以實現陣列自動擴容
* 儲存的圖片物件型別是: BufferedImage
* 目前實現了:
* add方法
* get方法
* remove方法
* size方法
*/
public class ImageArray {
private BufferedImage[] imgArray = {};
/**
* 陣列預設初始化容量
*/
private static final int defaultLength = 10;
private int size;
/**
* 陣列當前的空間容量
*/
private int length;
// 每張存入進來圖片的三 通道矩陣陣列
public ColorArray[] redArray = {};
public ColorArray[] greenArray = {};
public ColorArray[] blueArray = {};
public int getSize(){
return size;
}
//放大或縮小redArray的數值
public int[][] multiple(int multiple , ColorArray colorArray){
int w = colorArray.array.length;
int h = colorArray.array[0].length;
int[][] res = new int[w][h];
for (int i = 0; i < w; i++) {
for (int j = 0; j < h; j++) {
res[i][j] = Math.min(255,(int)(colorArray.array[i][j]*1.0*multiple/100));
}
}
return res;
}
/**
* 圖片動態陣列的初始化構造方法
*/
public ImageArray(int initSize){
if(initSize < defaultLength){
length = defaultLength;
imgArray = new BufferedImage[length];
redArray = new ColorArray[length];
greenArray = new ColorArray[length];
blueArray = new ColorArray[length];
size = 0;
}else{
length = initSize;
imgArray = new BufferedImage[length];
redArray = new ColorArray[length];
greenArray = new ColorArray[length];
blueArray = new ColorArray[length];
size = 0;
}
}
public void add(BufferedImage img){
if(size >= length){
int oldlength = length;
length = oldlength + oldlength>>1;
BufferedImage[] newArray = new BufferedImage[length];
for (int i = 0; i < oldlength; i++) {
newArray[i] = imgArray[i];
}
imgArray = newArray;
newArray = null;
}
imgArray[size] = img ;
redArray[size] = new ColorArray(img,ColorArray.TYPE_RED);
greenArray[size] = new ColorArray(img,ColorArray.TYPE_GREEN);
blueArray[size] = new ColorArray(img,ColorArray.TYPE_BLUE);
size++;
}
public void remove(int index) {
imgArray[index] = null;
size--;
}
//注意index的合法性
public BufferedImage get(int index) {
return imgArray[index] ;
}
}
ColorArray 二維陣列類,存放並處理顏色矩陣
package drawBoard_test2;
import java.awt.image.BufferedImage;
public class ColorArray{
static final int TYPE_RED = 0;
static final int TYPE_GREEN = 1;
static final int TYPE_BLUE = 2;
public int[][] array = {};
ColorArray(BufferedImage img , int type){
if(type == TYPE_RED){
array = new int[img.getWidth()][img.getHeight()];
for (int i = 0; i < img.getWidth(); i++) {
for (int j = 0; j < img.getHeight(); j++) {
array[i][j] = (img.getRGB(i,j)>>16) & 0xff;
}
}
}else if(type == TYPE_GREEN){
array = new int[img.getWidth()][img.getHeight()];
for (int i = 0; i < img.getWidth(); i++) {
for (int j = 0; j < img.getHeight(); j++) {
array[i][j] = (img.getRGB(i,j)>>8) & 0xff;
}
}
}else if(type == TYPE_BLUE){
array = new int[img.getWidth()][img.getHeight()];
for (int i = 0; i < img.getWidth(); i++) {
for (int j = 0; j < img.getHeight(); j++) {
array[i][j] = img.getRGB(i,j) & 0xff;
}
}
}
}
}
- 通過滑動條調整顏色,並繪製出來
@Override
public void stateChanged(ChangeEvent e) {
JSlider jSlider = (JSlider)e.getSource();
String s = jSlider.getToolTipText();
switch (s){
case "縮放比例":
multiple = jSlider.getValue();
int[][] img = getImagePixel(fileName);
BufferedImage bufferedImage = new BufferedImage(800,800,BufferedImage.TYPE_INT_ARGB);
Graphics buffg = bufferedImage.getGraphics();
drawImage_multiple(buffg,img);
g.drawImage(bufferedImage,0,0,null);
break;
case "紅色":
multipleRed = jSlider.getValue();
BufferedImage bufferedImage1 = new BufferedImage(800,800,BufferedImage.TYPE_INT_ARGB);
Graphics buffg1 = bufferedImage1.getGraphics();
drawImage_multiple_color(buffg1,imageArray);
g.drawImage(bufferedImage1,0,0,null);
break;
case "綠色":
multipleGreen = jSlider.getValue();
BufferedImage bufferedImage2 = new BufferedImage(800,800,BufferedImage.TYPE_INT_ARGB);
Graphics buffg2 = bufferedImage2.getGraphics();
drawImage_multiple_color(buffg2,imageArray );
g.drawImage(bufferedImage2,0,0,null);
break;
case "藍色":
multipleBlue = jSlider.getValue();
BufferedImage bufferedImage3 = new BufferedImage(800,800,BufferedImage.TYPE_INT_ARGB);
Graphics buffg3 = bufferedImage3.getGraphics();
drawImage_multiple_color(buffg3,imageArray);
g.drawImage(bufferedImage3,0,0,null);
break;
}
}
//注意:在圖片開啟的時候將從圖片提取出來的BufferedImage放入imageArray中
public void drawImage_multiple_color(Graphics g , ImageArray imageArray){
int index = imageArray.getSize()-1;
int w = (drawJPanel.getWidth()- imageArray.get(index).getWidth())/2;
int h = (drawJPanel.getHeight()- imageArray.get(index).getHeight())/2;
int[][] red ;
int[][] green;
int[][] blue ;
red = imageArray.multiple(multipleRed,imageArray.redArray[index]);
green = imageArray.multiple(multipleGreen,imageArray.greenArray[index]);
blue = imageArray.multiple(multipleBlue,imageArray.blueArray[index]);
for (int i = 0; i < imageArray.get(index).getWidth(); i++) {
for (int j = 0; j < imageArray.get(index).getHeight() ; j++) {
g.setColor(new Color(red[i][j],green[i][j],blue[i][j]));
g.drawRect(i+w,j+h,1,1);
}
}
}
第十二步:旋轉
- 拿向右旋轉來舉例,我們要把陣列向右旋轉變成一個新陣列,再輸出到螢幕上。
case "向左旋轉":
BufferedImage bufferedImage15 = new BufferedImage(800,800,BufferedImage.TYPE_INT_ARGB);
int img15[][] = getImagePixel(fileName);
img15 = RotateRight(img15);
Graphics buffg15 = bufferedImage15.getGraphics();
drawImage(buffg15,img15);
g.drawImage(bufferedImage15,0,0,null);
ImageShape imageShape15 = new ImageShape();
imageShape15.setBufferedImage(bufferedImage15);
shapeList.add(imageShape15);
break;
public int[][] RotateRight(int[][] img){
int w = img.length;
int h = img[0].length;
int[][] newImg = new int[h][w];
for (int i = 0; i < w; i++) {
for (int j = 0; j < h; j++) {
newImg[h-j-1][w-i-1] = img[i][j];
}
}
return newImg;
}![image](https://img2022.cnblogs.com/blog/2555328/202204/2555328-20220414151025986-1397523916.png)
效果圖片:
一點點心得總結
1、開始寫程式碼之前,一定要明確自己要實現什麼功能,達到什麼效果。
2、如何實現這樣的效果。
3、實現過程中:當前實現的效果是否符合預期,如果不符合要重新制定計劃。
4、蒐集資料,撰寫部落格,發現自己的不足,舊知新學。
原始碼以及圖片素材連結
提取碼:t1gp