此為一個基於 SWT/JFace + Apache Commons Net 的完整 FTP 客戶端範例。
org.eclipse.swt, org.eclipse.jface, org.eclipse.core.runtime, commons-net-ftp。ApplicationWindow (主視窗) + TableViewer (JFace Viewer 框架) + SashForm (分割面板) + 背景執行緒處理網路 IO。FTPWindow (主控制器), ConnectionDialog (連線設定), LocalDirectoryBrowser/RemoteDirectoryBrowser (Content/Label Provider), FileIconUtil (圖示快取)。StyledText 日誌區。ftp = new FTPClient();
ftp.addProtocolCommandListener(new ProtocolCommandListener() {
public void protocolCommandSent(ProtocolCommandEvent e) { logMessage("> " + e.getCommand(), false); }
public void protocolReplyReceived(ProtocolCommandEvent e) { logMessage("< " + e.getMessage(), false); }
});
// Action: Connect
ftp.connect(host, port);
if (!FTPReply.isPositiveCompletion(ftp.getReplyCode())) throw new RuntimeException("Refused");
ftp.login(user, pass);
labelPathRemote.setText("Path: " + ftp.printWorkingDirectory());
remoteDirBrowser.setInput(ftp.listFiles()); // 觸發 Viewer 刷新
來源:RemoteDirectoryBrowser (Table) -> 目標:LocalDirectoryBrowser (Table)。
event.data = fileName。FileOutputStream,呼叫 ftp.retrieveFile(fileName, stream) 下載。完成後 localDirBrowser.refresh()。File.isDirectory() -> setInput(dir) 更新路徑。FTPFile.isDirectory() -> ftp.changeWorkingDirectory(name) -> setInput(ftp.listFiles())。繼承 Dialog,使用 DialogSettings 載入/儲存 XML (ftp.connection.xml) 記住上次輸入的 Host, Port, User, Pass。
利用 Program.findProgram(extension) 取得系統關聯圖示,ImageRegistry 快取避免重複建立。支援 File 與 FTPFile 統一介面。
繼承 TableViewer,內建 IStructuredContentProvider (提供陣列資料) 與 ITableLabelProvider (提供圖示+三欄文字)。
// Local ContentProvider
public Object[] getElements(Object input) { return ((File)input).listFiles(); }
// LabelProvider Column 0: Icon, Name; Column 1: Size; Column 2: Date
package swt_jface.demo13;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import org.apache.commons.net.ProtocolCommandEvent;
import org.apache.commons.net.ProtocolCommandListener;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPReply;
import org.eclipse.jface.action.*;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.viewers.*;
import org.eclipse.jface.window.ApplicationWindow;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.dnd.*;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.*;
import org.eclipse.swt.widgets.*;
public class FTPWindow extends ApplicationWindow {
TableViewer localDirBrowser, remoteDirBrowser;
Label labelPathLocal, labelPathRemote;
StyledText textLog;
ConnectionDialog connectionDialog;
FTPClient ftp;
ConnectionInfo connectionInfo;
public FTPWindow(Shell parentShell) {
super(parentShell);
createActions();
addStatusLine();
addToolBar(SWT.FLAT);
addMenuBar();
ftp = new FTPClient();
ftp.addProtocolCommandListener(new ProtocolCommandListener() {
public void protocolCommandSent(ProtocolCommandEvent e) { logMessage("> " + e.getCommand(), false); }
public void protocolReplyReceived(ProtocolCommandEvent e) { logMessage("< " + e.getMessage(), false); }
});
}
// ... createActions() 定義 Action: connect, disconnect, upLocal, upRemote, browseLocal, about, exit ...
// ... createMenuManager() / createToolBarManager() 組裝選單與工具列 ...
@Override
protected Control createContents(Composite parent) {
Composite composite = new Composite(parent, SWT.NULL);
composite.setLayout(new FillLayout());
SashForm verticalForm = new SashForm(composite, SWT.VERTICAL);
SashForm horizontalForm = new SashForm(verticalForm, SWT.HORIZONTAL);
// --- Local Panel ---
Composite localComp = createDirPanel(horizontalForm, "Local", true);
// --- Remote Panel ---
Composite remoteComp = createDirPanel(horizontalForm, "Remote", false);
// --- Log Panel ---
textLog = new StyledText(verticalForm, SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL);
verticalForm.setWeights(new int[]{4, 1}); // 目錄區:日誌區 = 4:1
// 初始化本地根目錄
localDirBrowser.setInput(File.listRoots()[0]);
labelPathLocal.setText("Path: " + File.listRoots()[0]);
dragNDropSupport(); // 啟用拖曳
return composite;
}
private Composite createDirPanel(SashForm parent, String title, boolean isLocal) {
Composite comp = new Composite(parent, SWT.NULL);
comp.setLayout(new GridLayout());
Group top = new Group(comp, SWT.NULL);
top.setText(title);
top.setLayout(new GridLayout(isLocal ? 3 : 2, false));
top.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
Label pathLabel = new Label(top, SWT.NULL);
pathLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
pathLabel.setText("Path: ");
if (isLocal) labelPathLocal = pathLabel; else labelPathRemote = pathLabel;
Button btnUp = new Button(top, SWT.PUSH);
btnUp.setText("Up");
btnUp.addListener(SWT.Selection, e -> {
if (isLocal) actionUpLocalDir.run();
else actionUpRemoteDir.run();
});
if (isLocal) {
Button btnBrowse = new Button(top, SWT.PUSH);
btnBrowse.setText("Browse...");
btnBrowse.addListener(SWT.Selection, e -> actionBrowseLocalDir.run());
}
Table table = new Table(comp, SWT.BORDER);
new TableColumn(table, SWT.LEFT).setText("Name"); table.getColumn(0).setWidth(200);
new TableColumn(table, SWT.NULL).setText("Size"); table.getColumn(1).setWidth(100);
new TableColumn(table, SWT.NULL).setText("Date"); table.getColumn(2).setWidth(100);
table.setHeaderVisible(true);
table.setLayoutData(new GridData(GridData.FILL_BOTH));
if (isLocal) {
localDirBrowser = new LocalDirectoryBrowser(table);
table.addListener(SWT.MouseDoubleClick, e -> {
File f = (File) ((IStructuredSelection)localDirBrowser.getSelection()).getFirstElement();
if (f != null && f.isDirectory()) { localDirBrowser.setInput(f); labelPathLocal.setText("Path: " + f); }
});
} else {
remoteDirBrowser = new RemoteDirectoryBrowser(table);
table.addListener(SWT.MouseDoubleClick, e -> {
FTPFile f = (FTPFile) ((IStructuredSelection)remoteDirBrowser.getSelection()).getFirstElement();
if (f != null && f.isDirectory()) {
try { ftp.changeWorkingDirectory(f.getName());
labelPathRemote.setText("Path: " + ftp.printWorkingDirectory());
remoteDirBrowser.setInput(ftp.listFiles());
} catch (IOException ex) { logError(ex.toString()); }
}
});
}
return comp;
}
// ... dragNDropSupport(), logMessage(), logError(), main() 等輔助方法 ...
}
| 挑戰 | 解決方案 |
|---|---|
| 網路阻塞 UI | 所有 FTP 指令 (connect, listFiles, retrieveFile) 均在 UI 執行緒執行會卡死。本範例簡化為直接在 UI 執行緒呼叫,**生產環境必須移至背景執行緒** (如 Thread + asyncExec 或 Jobs API)。 |
| 被動/主動模式 | 預設主動模式。若需被動模式 (PASV) 穿透防火牆:ftp.enterLocalPassiveMode()。 |
| 編碼問題 | 中文檔名需設定 ftp.setControlEncoding("UTF-8") (Commons Net 3.0+)。 |
| 圖示效能 | FileIconUtil 使用 ImageRegistry 快取副檔名對應圖示,避免重複 Program.findProgram 呼叫。 |
| 資源洩漏 | FileOutputStream、FTPClient、Image、DragSource/DropTarget 皆需在 DisposeListener 中釋放。 |
ftp.storeFile()。org.apache.commons.net.ftp.FTPClientUtil 或自行實作 CopyStreamListener 監聽傳輸進度更新 UI。DialogSettings 或 XML 儲存多組連線設定,下拉選單快速切換。Job, IProgressMonitor) 統一管理背景任務、取消、排程。