一直在用android studio作android開發,偶然一次機會接觸到了wifi adb,試了好幾個支持android studio的插件,各有優缺點,有一個原本用着好好的,忽然就用不了,不懂啥狀況,因而就萌生了本身寫一個的想法。而後查了一下,感受寫android studio插件的話,會有點麻煩,寫一個小型PC桌面程序,相對簡單一些,但我也歷來沒寫過,固然,有着多年的Java基礎,也不慫,索性就,盤他。java
先上源碼地址:https://gitee.com/imxiaoyu_admin/WIFI-ADB.gitandroid
而後附上運行以後的效果圖:git
參考了一下其餘的插件,大概就是幾個功能點:shell
一、用戶手動輸入ip和端口號,直接鏈接設備數據庫
二、用戶先用usb鏈接手機,而後一鍵切換至wifi鏈接json
三、鏈接過的手機能夠保存起來(若是不保存的話,下次打開須要有限鏈接一次才能切換wifi鏈接,比較麻煩)緩存
四、鏈接、斷開等常規操做dom
五、手動設置adb程序的路徑(懶得寫自動檢測的,索性就寫個手動的,反正設置一次永久使用,問題不大)tcp
操做系統是win10,其餘一些Jdk等必要的東西,就不列舉了ide
一、開發工具Intellij Idea
二、adb.exe
網上下載一個gson.jar,而後複製到lib文件夾
點擊 File->Project Structure->Libraries,而後點加號並找到gson.jar,導入完成以後以下圖所示:
緩存邏輯:
由於程序比較簡單,這裏就不搞數據庫這種花裏胡哨的東西了,數據存儲的方式,就採用txt文件+json的形式,即在本機的某個固定路徑(這裏寫死D://wifi-adb-settings.txt)生成一個txt文件,用於存放相關數據的json字符串。
須要保存的,僅有adb路徑和歷史設備數據兩項,設備詳細信息我也不新建對象了,直接用Map<String,Objec>來處理了,一切從簡。
跟adb的交互,則是經過命令行的方式交互,經過執行命令行並解析結果,來完成相關的交互工做。
下面放工具類,具體邏輯很少說了,都有註釋,仔細看便可。
(1)CmdUtils
package com.imxiaoyu.wifi.adb.utils; import com.google.gson.Gson; import java.io.BufferedReader; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; public class CmdUtils { /** * 執行adb命令 * * @param cmd 要執行的命令 參數 * @return 返回執行結果,區分換行 */ public static List<String> runCmd(String cmd) { System.out.print("\n執行命令:" + cmd); List<String> list = new ArrayList<>(); Process proc = null; BufferedReader reader; String line = null; Runtime runtime = Runtime.getRuntime(); try { proc = runtime.exec(cmd); if (proc != null) { reader = new BufferedReader(new InputStreamReader(proc.getInputStream())); while ((line = reader.readLine()) != null) { if (line.length() > 1) { list.add(line); } } } } catch (Exception e) { System.out.print("執行命令:" + cmd + "出錯啦!"); return null; } System.out.print("執行結果:" + new Gson().toJson(list)); return list; } }
(2)ConnectUtils
package com.imxiaoyu.wifi.adb.utils; import com.google.gson.Gson; import javax.swing.*; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; public class ConnectUtils { /** * 連接設備 * @param name * @return */ public static boolean connect(String name) { String cmd = SettingsUtils.getSettings().getWifiAdbPath() + "/adb.exe " + "connect " + name; List<String> list = CmdUtils.runCmd(cmd); String str = new Gson().toJson(list); if (str.indexOf("connected to") > -1) { //連接成功 return true; } JOptionPane.showMessageDialog(null, "目標設備拒接,沒法連接。", "連接失敗", JOptionPane.ERROR_MESSAGE); return false; } /** * 斷開設備 * @param name * @return */ public static boolean disconnect(String name) { String cmd = SettingsUtils.getSettings().getWifiAdbPath() + "/adb.exe " + "disconnect " + name; List<String> list = CmdUtils.runCmd(cmd); String str = new Gson().toJson(list); if (str.indexOf("disconnected") > -1) { //斷開成功 return true; } JOptionPane.showMessageDialog(null, "設備不存在。", "斷開連接失敗", JOptionPane.ERROR_MESSAGE); return false; } /** * 解析一個usb設備的ip地址,不帶端口號 * * @return */ public static String getDeviceIp(String name) { String cmd = SettingsUtils.getSettings().getWifiAdbPath() + "/adb.exe" + " -s " + name + " shell ifconfig wlan0 "; List<String> list = CmdUtils.runCmd(cmd); if (list.size() < 2) { return null; } String str = list.get(1);//ip地址在第二行,實例參數: inet addr:192.168.22.11 Bcast:192.168.22.255 Mask:255.255.255.0 Pattern p = Pattern.compile("\\s+"); Matcher m = p.matcher(str); str = m.replaceAll(" ");//將多個連續的空格只保留一個 str = str.replace(":", " "); String[] strList = str.split(" "); System.out.print("\n讀取到的ip地址:" + strList[3]); return strList[3]; } /** * 給一個設備設置一個默認端口,默認5555,設置好了以後就能夠直接經過ip+端口連接 * * @param name * @param port * @return */ public static void setDevicePort(String name, String port) { String cmd = SettingsUtils.getSettings().getWifiAdbPath() + "/adb.exe" + " -s " + name + " tcpip " + port; CmdUtils.runCmd(cmd); } /** * 斷定一個設備是不是usb連接 * * @param name * @return true-usb連接 false-wifi連接 */ public static boolean isUsb(String name) { if (name.indexOf(":") == -1) { //若是設備的名稱帶有冒號的,證實是帶有端口的,也就不是usb的方式連接的了 return true; } return false; } /** * 斷定一個usb設備是否已經連接了wifi * * @param deviceList * @param name * @return true-usb連接 false-wifi連接 */ public static boolean isUsbConnectWifi(List<Map<String, Object>> deviceList, String name) { //斷定邏輯:先獲取這個有線設備的ip地址,若是現有的設備列表已經存在了這個ip地址,證實這個有線設備已經經過wifi連接了 String ip = getDeviceIp(name); for (int i = 0; i < deviceList.size(); i++) { String deviceName = deviceList.get(i).get("name") + ""; if (deviceName.startsWith(ip)) { return true; } } return false; } /** * 斷定一個wifi設備是否已經連接了 * * @param deviceList * @param name * @return true-usb連接 false-wifi連接 */ public static boolean isWifiConnect(List<Map<String, Object>> deviceList, String name) { //邏輯:若是這個wifi設備已經在當前的設備列表裏,那就是已鏈接了 for (int i = 0; i < deviceList.size(); i++) { String deviceName = deviceList.get(i).get("name") + ""; if (deviceName.startsWith(name)) { return true; } } return false; } }
(3)SettingsEntity
package com.imxiaoyu.wifi.adb.utils; import java.util.ArrayList; import java.util.List; import java.util.Map; public class SettingsEntity { private String wifiAdbPath = ""; private List<Map<String, Object>> phoneList=new ArrayList<>(); public String getWifiAdbPath() { return wifiAdbPath; } public void setWifiAdbPath(String wifiAdbPath) { this.wifiAdbPath = wifiAdbPath; } public List<Map<String, Object>> getPhoneList() { return phoneList; } public void setPhoneList(List<Map<String, Object>> phoneList) { this.phoneList = phoneList; } }
(4)SettingsUtils
package com.imxiaoyu.wifi.adb.utils; import com.google.gson.Gson; import javax.swing.*; import java.io.*; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; public class SettingsUtils { private static Gson gson = new Gson(); private static final String SETTINGS_PATH = "D://wifi-adb-settings.txt";//用於保存數據的txt文件的目錄 /** * 獲取當前連接的設備的設備列表 * * @return */ public static List<Map<String, Object>> getAllDevice() { List<Map<String, Object>> deviceList = new ArrayList<>(); SettingsEntity entity = getSettings(); String cmd = entity.getWifiAdbPath() + "/adb.exe " + "devices -l";//獲取列表的命令 List<String> cmdList = CmdUtils.runCmd(cmd); //運行adb.exe devices -l 這個命令以後,從返回的結果中解析出設備的相關信息 for (int i = 1; i < cmdList.size(); i++) { //跳過第一行 Pattern p = Pattern.compile("\\s+"); Matcher m = p.matcher(cmdList.get(i)); String str = m.replaceAll(" ");//將多個連續的空格只保留一個 str = str.replace(":", " ");//將冒號抓換成空格,方便解析 if (str.indexOf(".") > -1) { //若是包含有小數點,證實名字是帶端口的,這時候須要把第一個空格還原爲冒號 str = str.replaceFirst(" ", ":"); } System.out.print("\n解析前:" + str); Map<String, Object> map = new HashMap<>(); String[] list = str.split(" "); map.put("name", list[0]); map.put("device_product", list[3]); map.put("model", list[5]); map.put("device", list[7]); map.put("transport_id", list[9]); deviceList.add(map); } return deviceList; } /** * 將一個已鏈接的設備保存起來 * * @param map */ public static void saveDevice(Map<String, Object> map) { if (isHaveDevice(map)) { JOptionPane.showMessageDialog(null, "設備已存在", "保存失敗", JOptionPane.ERROR_MESSAGE); return; } SettingsEntity entity = getSettings(); map.put("id", UUID.randomUUID().toString());//保存的時候給加一個惟一id,方便刪除 entity.getPhoneList().add(map); setSettings(entity); //保存成功 } /** * 根據id,刪除一個已保存的設備 * * @param id */ public static void removeDevice(String id) { SettingsEntity entity = getSettings(); List<Map<String, Object>> list = getSettings().getPhoneList(); for (int i = 0; i < list.size(); i++) { if (list.get(i).get("id").equals(id)) { list.remove(i); } entity.setPhoneList(list); setSettings(entity); return; } //刪除成功 } /** * 斷定一個設備是否已經存在 * * @param map * @return */ public static boolean isHaveDevice(Map<String, Object> map) { List<Map<String, Object>> deviceList = getSettings().getPhoneList(); if (deviceList == null) { deviceList = new ArrayList<>(); } boolean isHave = false;//是否存在 for (int i = 0; i < deviceList.size(); i++) { if (deviceList.get(i).get("name").equals(map.get("name")) && deviceList.get(i).get("model").equals(map.get("model"))) { isHave = true;//這個設備已存在 } } return isHave; } /** * 獲取保存的數據 * * @return */ public static SettingsEntity getSettings() { SettingsEntity settingsEntity; String str = readString(); settingsEntity = gson.fromJson(str, SettingsEntity.class); if (settingsEntity == null) { settingsEntity = new SettingsEntity(); } return settingsEntity; } /** * 寫入保存數據 * * @param entity */ public static void setSettings(SettingsEntity entity) { writeString(gson.toJson(entity)); } /** * 從文件中讀取保存數據的json字符串 * * @return */ private static String readString() { File file = new File(SETTINGS_PATH); if (!file.exists()) { try { file.createNewFile(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } try (FileReader reader = new FileReader(SETTINGS_PATH); BufferedReader br = new BufferedReader(reader) // 創建一個對象,它把文件內容轉成計算機能讀懂的語言 ) { String allStr = ""; String line; //網友推薦更加簡潔的寫法 while ((line = br.readLine()) != null) { // 一次讀入一行數據 // System.out.println(line); allStr += line; } return allStr; } catch (IOException e) { e.printStackTrace(); } return null; } /** * 將json字符串寫入到txt文件中 * * @param settingsStr */ private static void writeString(String settingsStr) { try { File writeName = new File(SETTINGS_PATH); // 相對路徑,若是沒有則要創建一個新的output.txt文件 writeName.createNewFile(); // 建立新文件,有同名的文件的話直接覆蓋 try (FileWriter writer = new FileWriter(writeName); BufferedWriter out = new BufferedWriter(writer) ) { out.write(settingsStr); // \r\n即爲換行 out.flush(); // 把緩存區內容壓入文件 } } catch (IOException e) { e.printStackTrace(); } } }
其實寫頁面是我耗時最久的地方,多是寫習慣android了,切換到這邊,寫列表的時候,習慣性使用jlist這個控件,後來試了挺屢次了,效果不太滿意,後來查了一下,才轉過來用jtabel,對是用對了,可是這個坑也挺坑的,反正就是遇到各類問題,耗費了很多時間。
編寫過程也很少少了,直接看代碼吧,都有註釋,先上一張截圖,而後是源碼。
頁面相關的操做邏輯,我都放到了IndexView這個頁面裏,可能會顯得稍亂一些。
package com.imxiaoyu.wifi.adb.view; import com.imxiaoyu.wifi.adb.utils.ConnectUtils; import com.imxiaoyu.wifi.adb.utils.SettingsEntity; import com.imxiaoyu.wifi.adb.utils.SettingsUtils; import javax.swing.*; import javax.swing.table.AbstractTableModel; import javax.swing.table.TableCellEditor; import javax.swing.table.TableCellRenderer; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.ArrayList; import java.util.List; import java.util.Map; public class IndexView { private JPanel jPanel; private JTextField tf_adb_path; private JButton btn_adb_path; private JButton btn_refresh; private JTable tb_now; private JTable tb_history; private JButton btn_test_connect; private JTextField tf_ip_port; private List<Map<String, Object>> nowDeviceList = new ArrayList<>(); private List<Map<String, Object>> historyDeviceList = new ArrayList<>(); public void startView() { JFrame frame = new JFrame("Wifi Adb"); frame.setContentPane(jPanel); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.pack(); frame.setVisible(true); frame.setSize(new Dimension(880, 500)); btn_adb_path.addActionListener(new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { //設置一下adb.exe的路徑 JFileChooser fileChooser = new JFileChooser("D:\\"); fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); int returnVal = fileChooser.showOpenDialog(fileChooser); if (returnVal == JFileChooser.APPROVE_OPTION) { String filePath = fileChooser.getSelectedFile().getAbsolutePath();//這個就是你選擇的文件夾的路徑 tf_adb_path.setText(filePath); System.out.print("\n選中的路徑:" + filePath); SettingsEntity entity = SettingsUtils.getSettings(); entity.setWifiAdbPath(filePath); SettingsUtils.setSettings(entity); } } }); btn_test_connect.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { //經過手動輸入ip和端口的形式,直接鏈接設備 String deviceName=tf_ip_port.getText(); ConnectUtils.connect(deviceName); initList(); } }); btn_refresh.addActionListener(new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { //刷新列表 initList(); } }); //初始化adv.exe的路徑信息 SettingsEntity entity = SettingsUtils.getSettings(); tf_adb_path.setText(entity.getWifiAdbPath()); initList(); } private void initList() { nowDeviceList = SettingsUtils.getAllDevice(); NowListModel listModel = new NowListModel(); tb_now.setModel(listModel);//設置須要顯示的數據 tb_now.setRowHeight(36); //這個setCellEditor和setCellRenderer必須分別new,否則的話點擊按鈕的時候會有點小問題 tb_now.getColumnModel().getColumn(3).setCellEditor(new NowListEditor());//加入點擊監聽 tb_now.getColumnModel().getColumn(3).setCellRenderer(new NowListEditor());//加入操做按鈕 tb_now.setRowSelectionAllowed(false); historyDeviceList = SettingsUtils.getSettings().getPhoneList(); HistoryListModel historyListModel = new HistoryListModel(); tb_history.setModel(historyListModel); tb_history.setRowHeight(36); tb_history.getColumnModel().getColumn(3).setCellEditor(new HistoryListEditor()); tb_history.getColumnModel().getColumn(3).setCellRenderer(new HistoryListEditor()); tb_history.setRowSelectionAllowed(false); } public class NowListModel extends AbstractTableModel { /** * 獲得表格行數 */ @Override public int getRowCount() { if (nowDeviceList.size() == 0) { //列表空的話,標題也不顯示 return 0; } return nowDeviceList.size() + 1; } /** * 重寫方法,獲得表格列數 */ @Override public int getColumnCount() { return 4; } @Override public Object getValueAt(int rowIndex, int columnIndex) { if (rowIndex == 0) { if (columnIndex == 0) { return "型號"; } if (columnIndex == 1) { return "設備名稱"; } if (columnIndex == 2) { return "transport_id"; } if (columnIndex == 3) { return "操做"; } } if (columnIndex == 0) { return nowDeviceList.get(rowIndex - 1).get("model"); } if (columnIndex == 1) { return nowDeviceList.get(rowIndex - 1).get("name"); } if (columnIndex == 2) { return nowDeviceList.get(rowIndex - 1).get("transport_id"); } return ""; } @Override public boolean isCellEditable(int row, int column) { // 帶有按鈕列的功能這裏必需要返回true否則按鈕點擊時不會觸發編輯效果,也就不會觸發事件。 if (column == 3) { return true; } else { return false; } } } public class HistoryListModel extends AbstractTableModel { /** * 獲得表格行數 */ @Override public int getRowCount() { if (historyDeviceList.size() == 0) { return 0; } return historyDeviceList.size() + 1; } /** * 重寫方法,獲得表格列數 */ @Override public int getColumnCount() { return 4; } @Override public Object getValueAt(int rowIndex, int columnIndex) { if (rowIndex == 0) { if (columnIndex == 0) { return "型號"; } if (columnIndex == 1) { return "設備名稱"; } if (columnIndex == 3) { return "操做"; } if (columnIndex == 2) { return "transport_id"; } } if (columnIndex == 1) { return historyDeviceList.get(rowIndex - 1).get("name"); } if (columnIndex == 0) { return historyDeviceList.get(rowIndex - 1).get("model"); } if (columnIndex == 2) { return historyDeviceList.get(rowIndex - 1).get("transport_id"); } return ""; } @Override public boolean isCellEditable(int row, int column) { // 帶有按鈕列的功能這裏必需要返回true否則按鈕點擊時不會觸發編輯效果,也就不會觸發事件。 if (column == 3) { return true; } else { return false; } } } public class NowListEditor extends AbstractCellEditor implements TableCellRenderer, TableCellEditor { private JPanel panel; private JButton btnSave = null; private JButton btnDis = null; private JButton btnConnect = null; private int clickRow; public NowListEditor() { initButton(); initPanel(); panel.add(btnConnect); panel.add(btnDis); panel.add(btnSave); } private void initButton() { /** * 初始化按鈕 */ btnConnect = new JButton("連接"); btnDis = new JButton("斷開"); btnSave = new JButton("保存"); /** * 設置按鈕監聽 */ btnConnect.addActionListener(new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { System.out.print("\n連接"); System.out.print(";clickRow:" + clickRow); String deviceName = nowDeviceList.get(clickRow - 1).get("name") + ""; String ip = ConnectUtils.getDeviceIp(deviceName); String port = "5555"; ConnectUtils.setDevicePort(deviceName, port); ConnectUtils.connect(ip + ":" + port); initList(); } }); btnDis.addActionListener(new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { System.out.print("\n斷開"); System.out.print(";clickRow:" + clickRow); ConnectUtils.disconnect(nowDeviceList.get(clickRow - 1).get("name") + ""); initList(); } }); btnSave.addActionListener(new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { System.out.print("\n保存"); System.out.print(";clickRow:" + clickRow); SettingsUtils.saveDevice(nowDeviceList.get(clickRow - 1)); initList(); } }); } /** * 初始化畫板 */ private void initPanel() { panel = new JPanel(); } @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { /** * 這裏是處理顯示 */ panel.setBackground(new Color(255, 255, 255)); if (row != 0) { panel.removeAll(); String deviceName = nowDeviceList.get(row - 1).get("name") + ""; if (ConnectUtils.isUsb(deviceName)) { //這是一個有線設備 if (!ConnectUtils.isUsbConnectWifi(nowDeviceList, deviceName)) { //這個有限設備還沒連接wifi panel.add(btnConnect); } } else { //這是一個wifi設備 panel.add(btnDis); if (!SettingsUtils.isHaveDevice(nowDeviceList.get(row - 1))) { //這個wifi設備尚未保存 panel.add(btnSave); } } return panel; } return null; } @Override public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { /** * 這裏是處理點擊 */ clickRow = row; panel.setBackground(new Color(255, 255, 255)); if (row != 0) { panel.removeAll(); String deviceName = nowDeviceList.get(row - 1).get("name") + ""; if (ConnectUtils.isUsb(deviceName)) { System.out.print("\n有線設備:"+deviceName); //這是一個有線設備 if (!ConnectUtils.isUsbConnectWifi(nowDeviceList, deviceName)) { //這個有限設備還沒連接wifi panel.add(btnConnect);//顯示鏈接按鈕 } //若是有線設備已經連接了,就再也不顯示鏈接按鈕 } else { //這是一個wifi設備 panel.add(btnDis);//若是列表裏面有wifi設備,證實確定是連接狀態了的,因此只須要顯示斷開按鈕 if (!SettingsUtils.isHaveDevice(nowDeviceList.get(row - 1))) { //這個wifi設備尚未保存 panel.add(btnSave);//沒保存就顯示保存按鈕,已保存則不顯示 } } return panel; } return null; } @Override public Object getCellEditorValue() { return null; } } public class HistoryListEditor extends AbstractCellEditor implements TableCellRenderer, TableCellEditor { private JPanel panel; private JButton btnSave = null; private JButton btnDis = null; private JButton btnConnect = null; private int clickRow; public HistoryListEditor() { initButton(); initPanel(); panel.add(btnConnect); panel.add(btnDis); panel.add(btnSave); } private void initButton() { /** * 初始化按鈕 */ btnConnect = new JButton("連接"); btnDis = new JButton("斷開"); btnSave = new JButton("刪除"); /** * 監聽點擊 */ btnConnect.addActionListener(new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { System.out.print("\n連接"); ConnectUtils.connect(historyDeviceList.get(clickRow - 1).get("name") + ""); System.out.print(";clickRow:" + clickRow); initList(); } }); btnDis.addActionListener(new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { System.out.print("\n斷開"); ConnectUtils.disconnect(historyDeviceList.get(clickRow - 1).get("name") + ""); System.out.print(";clickRow:" + clickRow); initList(); } }); btnSave.addActionListener(new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { System.out.print("\n刪除"); System.out.print(";clickRow:" + clickRow); SettingsUtils.removeDevice(historyDeviceList.get(clickRow - 1).get("id") + ""); initList(); } }); } /** * 初始化畫板 */ private void initPanel() { panel = new JPanel(); } @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { panel.setBackground(new Color(255, 255, 255)); if (row != 0) { panel.removeAll(); String deviceName=historyDeviceList.get(row-1).get("name")+""; if(ConnectUtils.isWifiConnect(nowDeviceList,deviceName)){ System.out.print("wifi已鏈接"); //這個wifi設備已經連接 panel.add(btnDis);//已鏈接,顯示斷開按鈕 }else{ //還沒連接 panel.add(btnConnect);//未鏈接,顯示鏈接按鈕 } panel.add(btnSave);//顯示刪除按鈕 return panel; } return null; } @Override public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { clickRow = row; panel.setBackground(new Color(255, 255, 255)); if (row != 0) { panel.removeAll(); String deviceName=historyDeviceList.get(row-1).get("name")+""; if(ConnectUtils.isWifiConnect(nowDeviceList,deviceName)){ //這個wifi設備已經連接 panel.add(btnDis);//已鏈接,顯示斷開按鈕 }else{ //還沒連接 panel.add(btnConnect);//未鏈接,顯示鏈接按鈕 } panel.add(btnSave);//顯示刪除按鈕 return panel; } return null; } @Override public Object getCellEditorValue() { return null; } } }
點擊Build->Build Artifacts->All Artifacts->Build
Idea會自動在out目錄中生成jar包