第19章:项目实战 - 图片浏览器
作者:步子哥 (steper@foxmail.com)
19.1 需求分析
功能列表
图片浏览器 = 电子相册
- 就像你在电脑上浏览照片
- 但可以在电脑上缩放、旋转、全屏浏览
类比:
图片浏览器 = 画展
- 画展上展出多张照片(图片列表)
- 你可以放大看细节(缩放)
- 你可以旋转看不同角度(旋转)
- 你可以全屏欣赏(全屏模式)
核心功能:
- 打开图片
- 从文件管理器拖放图片 - 通过文件对话框选择图片 - 支持多种格式(JPG、PNG、GIF、BMP)
- 图片浏览
- 上一张/下一张 - 幻动轮播 - 缩放(放大/缩小/适应窗口) - 旋转(向左/向右 90 度)
- 图像操作
- 保存图片 - 全屏模式 - 删除图片 - 图片信息显示
- 帮助功能
- 快捷键支持 - 工具栏 - 状态栏显示
技术选型
技术选型 = 工具选择
- 图片显示:SWT Canvas + GC(绘制图片)
- 图像加载:SWT Image(加载图片文件)
- 缩放旋转:GC 变换(图像变换)
- 快捷键:SWT 键盘事件监听
- 布局管理:GridLayout(网格布局)
类比:
技术选型 = 厨房工具选择
- 图片显示 = 墙壁(绘制背景)
- 图像加载 = 画笔(绘制细节)
- 缩放旋转 = 尺子(调整大小)
- 布局管理 = 蓝图(规划空间)
技术栈:
- GUI 库:SWT 3.124.200
- 语言:Java 11+
- 构建工具:Maven
- 支持格式:JPG、PNG、GIF、BMP
19.2 界面设计
主窗口布局
主窗口 = 画廊
┌─────────────────────────────────────┐
│ 菜单栏(File, View, Help) │
├─────────────────────────────────────┤
│ 工具栏(打开、上一张、下一张、缩放、旋转、全屏) │
├─────────────────────────────────────┤
│ │
│ 图片显示区域 │
│ (Canvas + GC) │
│ │
│ │
├─────────────────────────────────────┤
│ 状态栏(文件名、尺寸、格式) │
└─────────────────────────────────────┘
费曼解释:主窗口的布局
主窗口 = 电影院
- 菜单栏 = 电影片头(电影名、播放时间)
- 工具栏 = 控制台(播放、暂停、音量)
- 图片显示区域 = 屏幕(显示电影)
- 状态栏 = 字幕(显示当前信息)
类比:
主窗口 = 画室
- 菜单栏 = 画室招牌(画室名、营业时间)
- 工具栏 = 画笔架(画笔、调色板、画板)
- 图片显示区域 = 画布(展示画作)
- 状态栏 = 标签(画作名、作者、年份)
图片显示区域
// 创建图片显示区域
Canvas canvas = new Canvas(shell, SWT.BORDER | SWT.DOUBLE_BUFFERED);
GridData canvasData = new GridData();
canvasData.horizontalAlignment = GridData.FILL;
canvasData.verticalAlignment = GridData.FILL;
canvasData.grabExcessHorizontalSpace = true;
canvasData.grabExcessVerticalSpace = true;
canvas.setLayoutData(canvasData);
// 绘制图片
canvas.addPaintListener(event -> {
GC gc = event.gc;
Rectangle clientArea = canvas.getClientArea();
// 绘制背景
gc.setBackground(display.getSystemColor(SWT.COLOR_BLACK));
gc.fillRectangle(clientArea);
// 绘制图片
if (currentImage != null) {
Rectangle imageBounds = calculateImageBounds(clientArea);
gc.drawImage(currentImage, imageBounds.x, imageBounds.y,
imageBounds.width, imageBounds.height);
}
});
费曼解释:图片显示区域的原理
图片显示区域 = 相框
- 相框(Canvas):显示照片的地方
- 照片(Image):实际的图像
- 算法(calculateImageBounds):计算照片在相框中的位置和大小
类比:
图片显示区域 = 画布
- 画布(Canvas):展示画作的地方
- 画作(Image):实际的画作
- 算法(calculateImageBounds):计算画作在画布中的位置和大小
19.3 数据模型
图片的数据结构
class ImageInfo {
private String filePath; // 文件路径
private String fileName; // 文件名
private String format; // 格式(JPG、PNG、GIF、BMP)
private int width; // 宽度
private int height; // 高度
private long fileSize; // 文件大小
private long lastModified; // 最后修改时间
// 旋转角度(0、90、180、270)
private int rotation;
// 缩放比例(1.0、1.5、2.0、0.5)
private double zoom;
public ImageInfo() {
this.rotation = 0;
this.zoom = 1.0;
}
// Getter 和 Setter
public String getFilePath() { return filePath; }
public void setFilePath(String filePath) { this.filePath = filePath; }
public String getFileName() { return fileName; }
public void setFileName(String fileName) { this.fileName = fileName; }
public String getFormat() { return format; }
public void setFormat(String format) { this.format = format; }
public int getWidth() { return width; }
public void setWidth(int width) { this.width = width; }
public int getHeight() { return height; }
public void setHeight(int height) { this.height = height; }
public long getFileSize() { return fileSize; }
public void setFileSize(long fileSize) { this.fileSize = fileSize; }
public long getLastModified() { return lastModified; }
public void setLastModified(long lastModified) { this.lastModified = lastModified; }
public int getRotation() { return rotation; }
public void setRotation(int rotation) { this.rotation = rotation; }
public double getZoom() { return zoom; }
public void setZoom(double zoom) { this.zoom = zoom; }
}
费曼解释:图片信息的数据结构
图片信息 = 产品标签
- 文件路径 = 产品编号
- 文件名 = 产品名称
- 格式 = 产品类型
- 尺寸 = 产品规格
- 文件大小 = 产品重量
- 最后修改 = 生产日期
- 旋转角度 = 展示角度
- 缩放比例 = 展示大小
类比:
图片信息 = 图书馆藏书信息
- 文件路径 = 索书号
- 文件名 = 书名
- 格式 = 文学类型(小说、散文、诗歌)
- 尺寸 = 书本大小(32 开、16 开、8 开)
- 文件大小 = 书本页数
- 最后修改 = 借阅日期
- 旋转角度 = 阅览角度
- 缩放比例 = 阅览大小(字体大小)
图片列表的管理
class ImageList {
private List<ImageInfo> images = new ArrayList<>();
private int currentIndex = -1;
// 添加图片
public void addImage(ImageInfo image) {
images.add(image);
if (currentIndex == -1) {
currentIndex = 0;
}
}
// 获取当前图片
public ImageInfo getCurrentImage() {
if (currentIndex >= 0 && currentIndex < images.size()) {
return images.get(currentIndex);
}
return null;
}
// 获取上一张图片
public ImageInfo getPreviousImage() {
if (currentIndex > 0) {
currentIndex--;
return images.get(currentIndex);
}
return null;
}
// 获取下一张图片
public ImageInfo getNextImage() {
if (currentIndex < images.size() - 1) {
currentIndex++;
return images.get(currentIndex);
}
return null;
}
// 获取指定索引的图片
public ImageInfo getImage(int index) {
if (index >= 0 && index < images.size()) {
return images.get(index);
}
return null;
}
// 获取图片数量
public int size() {
return images.size();
}
// 清空图片列表
public void clear() {
images.clear();
currentIndex = -1;
}
}
费曼解释:图片列表的管理
图片列表 = 幻灯片
- 幻灯片列表(images):所有幻灯片
- 当前幻灯片(currentIndex):当前播放的幻灯片
- 上一张(getPreviousImage):回到上一张
- 下一张(getNextImage):播放下一张
- 指定幻灯片(getImage):跳到指定幻灯片
类比:
图片列表 = 播放列表
- 播放列表(images):所有歌曲
- 当前歌曲(currentIndex):当前播放的歌曲
- 上一首(getPreviousSong):回到上一首
- 下一首(getNextSong):播放下一首
- 指定歌曲(getSong):跳到指定歌曲
19.4 功能实现
打开图片
// 通过文件对话框打开图片
File file = new FileDialog(shell, SWT.OPEN).open();
if (file != null) {
try {
// 创建图片信息
ImageInfo imageInfo = new ImageInfo();
imageInfo.setFilePath(file.getAbsolutePath());
imageInfo.setFileName(file.getName());
imageInfo.setFormat(getFileFormat(file));
imageInfo.setFile(file.length());
imageInfo.setLastModified(file.lastModified());
// 加载图片
Image image = new Image(display, file.getAbsolutePath());
ImageData imageData = image.getImageData();
imageInfo.setWidth(imageData.width);
imageInfo.setHeight(imageData.height);
image.dispose();
// 添加到图片列表
imageList.addImage(imageInfo);
// 加载当前图片
loadCurrentImage();
// 更新 UI
updateUI();
} catch (Exception e) {
MessageBox messageBox = new MessageBox(shell, SWT.ICON_ERROR | SWT.OK);
messageBox.setText("错误");
messageBox.setMessage("无法打开图片:" + e.getMessage());
messageBox.open();
}
}
// 获取文件格式
private String getFileFormat(File file) {
String name = file.getName().toLowerCase();
if (name.endsWith(".jpg") || name.endsWith(".jpeg")) {
return "JPG";
} else if (name.endsWith(".png")) {
return "PNG";
} else if (name.endsWith(".gif")) {
return "GIF";
} else if (name.endsWith(".bmp")) {
return "BMP";
}
return "Unknown";
}
费曼解释:打开图片的流程
打开图片 = 挂照片到墙上
- 你从相册(文件对话框)选择照片
- 拿照片到墙上(加载图片)
- 照片挂在墙上(添加到图片列表)
- 看到照片(显示图片)
类比:
打开图片 = 把书架上的书放到桌子上
- 你从书架(文件对话框)选择书
- 把书放到桌子上(加载图片)
- 把书加入阅读列表(添加到图片列表)
- 开始看书(显示图片)
缩放图片
// 放大图片
public void zoomIn() {
ImageInfo currentImage = imageList.getCurrentImage();
if (currentImage == null) {
return;
}
currentImage.setZoom(Math.min(currentImage.getZoom() + 0.25, 4.0));
updateUI();
}
// 缩小图片
public void zoomOut() {
ImageInfo currentImage = imageList.getCurrentImage();
if (currentImage == null) {
return;
}
currentImage.setZoom(Math.max(currentImage.getZoom() - 0.25, 0.25));
updateUI();
}
// 适应窗口
public void fitToWindow() {
ImageInfo currentImage = imageList.getCurrentImage();
if (currentImage == null) {
return;
}
// 计算适应窗口的缩放比例
Rectangle clientArea = canvas.getClientArea();
double scaleX = (double) clientArea.width / currentImage.getWidth();
double scaleY = (double) clientArea.height / currentImage.getHeight();
currentImage.setZoom(Math.min(scaleX, scaleY));
updateUI();
}
// 恢复原始大小
public void resetZoom() {
ImageInfo currentImage = imageList.getCurrentImage();
if (currentImage == null) {
return;
}
currentImage.setZoom(1.0);
updateUI();
}
// 计算图片显示区域
private Rectangle calculateImageBounds(Rectangle clientArea) {
ImageInfo currentImage = imageList.getCurrentImage();
if (currentImage == null) {
return null;
}
int imageWidth = (int)(currentImage.getWidth() * currentImage.getZoom());
int imageHeight = (int)(currentImage.getHeight() * currentImage.getZoom());
int x = (clientArea.width - imageWidth) / 2;
int y = (clientArea.height - imageHeight) / 2;
return new Rectangle(x, y, imageWidth, imageHeight);
}
费曼解释:缩放图片的原理
缩放图片 = 放大/缩小照片
- 原图:100x100 像素
- 放大 2 倍:200x200 像素(图片变大)
- 缩小 0.5 倍:50x50 像素(图片变小)
类比:
缩放图片 = 放大/缩小打印
- 原图:A4 纸 100%
- 放大 1.5 倍:A3 纸 150%
- 缩小 0.7 倍:A5 纸 70%
旋转图片
// 向左旋转 90 度
public void rotateLeft() {
ImageInfo currentImage = imageList.getCurrentImage();
if (currentImage == null) {
return;
}
currentImage.setRotation((currentImage.getRotation() + 270) % 360);
updateUI();
}
// 向右旋转 90 度
public void rotateRight() {
ImageInfo currentImage = imageList.getCurrentImage();
if (currentImage == null) {
return;
}
currentImage.setRotation((currentImage.getRotation() + 90) % 360);
updateUI();
}
// 翻转 180 度
public void flip() {
ImageInfo currentImage = imageList.getCurrentImage();
if (currentImage == null) {
return;
}
currentImage.setRotation((currentImage.getRotation() + 180) % 360);
updateUI();
}
// 重置旋转
public void resetRotation() {
ImageInfo currentImage = imageList.getCurrentImage();
if (currentImage == null) {
return;
}
currentImage.setRotation(0);
updateUI();
}
// 绘制旋转后的图片
private void drawRotatedImage(GC gc, Image image, int x, int y, int width, int height, int rotation) {
if (rotation == 0) {
gc.drawImage(image, x, y, width, height);
return;
}
// 旋转图片中心
Point center = new Point(x + width / 2, y + height / 2);
// 应用变换
Transform transform = new Transform(display);
transform.translate(center.x, center.y);
transform.rotate((float)Math.toRadians(rotation));
transform.translate(-center.x, -center.y);
gc.setTransform(transform);
gc.drawImage(image, x, y, width, height);
gc.setTransform(null);
}
费曼解释:旋转图片的原理
旋转图片 = 旋转照片
- 原图:风景照,天空在上面,地在下面
- 旋转 90 度:天空在左边,地在右边
- 旋转 180 度:天空在下面,地在上边(倒过来了)
- 旋转 270 度:天空在右边,地在左边
类比:
旋转图片 = 旋转画布
- 原图:画上有天空和地面(天空在上,地在下)
- 旋转 90 度:天空在左,地在右(画布旋转了 90 度)
- 旋转 180 度:天空在下,地在上(画布旋转了 180 度)
- 旋转 270 度:天空在右,地在左(画布旋转了 270 度)
全屏模式
// 切换全屏模式
public void toggleFullScreen() {
shell.setFullScreen(!shell.getFullScreen());
}
// 退出全屏(按 ESC 键)
shell.addListener(SWT.KeyDown, event -> {
if (event.keyCode == SWT.ESC && shell.getFullScreen()) {
shell.setFullScreen(false);
}
});
费曼解释:全屏模式的含义
全屏模式 = 沉浸式观影
- 普通模式:看到窗口边框、菜单栏、任务栏(有干扰)
- 全屏模式:只看到电影,看不到其他东西(无干扰)
类比:
全屏模式 = 沉浸式阅读
- 普通模式:可以看到书页、翻页器、书签(有干扰)
- 全屏模式:只看到文字,看不到其他东西(无干扰)
拖放打开图片
// 创建拖放目标
DropTarget dropTarget = new DropTarget(canvas, DND.DROP_COPY);
dropTarget.setTransfer(new Transfer[]{FileTransfer.getInstance()});
dropTarget.addDropListener(new DropTargetAdapter() {
@Override
public void dragEnter(DropTargetEvent event) {
if (event.detail == DND.DROP_NONE) {
event.detail = DND.DROP_COPY;
}
}
@Override
public void drop(DropTargetEvent event) {
if (FileTransfer.getInstance().isSupportedType(event.currentDataType)) {
String[] fileNames = (String[]) event.data;
for (String fileName : fileNames) {
openImage(fileName);
}
}
}
});
费曼解释:拖放打开的原理
拖放打开 = 把照片从相册拖到墙上
- 你从相册(文件管理器)选择照片
- 拖到墙上(拖放目标)
- 照片贴在墙上(打开图片)
类比:
拖放打开 = 把书从书架拿到桌上
- 你从书架(文件管理器)选择书
- 拿到桌上(拖放目标)
- 把书放在桌上(打开图片)
19.5 完整代码
public class ImageViewerApp {
private Display display;
private Shell shell;
private Canvas canvas;
private Image currentImage;
private ImageList imageList;
private ToolBar toolBar;
private StatusLine statusLine;
private static final double[] ZOOM_LEVELS = {0.25, 0.5, 0.75, 1.0, 1.5, 2.0, 3.0, 4.0};
private int currentZoomLevel = 3; // 默认 1.0 倍
private int rotation = 0; // 默认 0 度
public static void main(String[] args) {
new ImageViewerApp().open();
}
public void open() {
display = new Display();
shell = new Shell(display);
shell.setText("图片浏览器");
shell.setLayout(new GridLayout(1, false));
// 菜单栏
createMenuBar();
// 工具栏
createToolBar();
// 图片显示区域
canvas = new Canvas(shell, SWT.BORDER | SWT.DOUBLE_BUFFERED);
GridData canvasData = new GridData();
canvasData.horizontalAlignment = GridData.FILL;
canvasData.verticalAlignment = GridData.FILL;
canvasData.grabExcessHorizontalSpace = true;
canvasData.grabExcessVerticalSpace = true;
canvasData.heightHint = 400;
canvas.setLayoutData(canvasData);
canvas.addPaintListener(event -> {
paintCanvas(event.gc);
});
// 拖放支持
DropTarget dropTarget = new DropTarget(canvas, DND.DROP_COPY);
dropTarget.setTransfer(new Transfer[]{FileTransfer.getInstance()});
dropTarget.addDropListener(new DropTargetAdapter() {
@Override
public void drop(DropTargetEvent event) {
if (FileTransfer.getInstance().isSupportedType(event.currentDataType)) {
String[] fileNames = (String[]) event.data;
for (String fileName : fileNames) {
openImage(fileName);
}
}
}
});
// 键盘监听
shell.addListener(SWT.KeyDown, event -> {
if (event.keyCode == SWT.ARROW_LEFT) {
previousImage();
} else if (event.keyCode == SWT.ARROW_RIGHT) {
nextImage();
} else if (event.keyCode == SWT.PLUS) {
zoomIn();
} else if (event == SWT.MINUS) {
zoomOut();
} else if (event.keyCode == SWT.ESC && shell.getFullScreen()) {
shell.setFullScreen(false);
}
});
// 窗口大小改变时重绘
canvas.addListener(SWT.Resize, event -> {
canvas.redraw();
});
// 状态栏
statusLine = new StatusLine(shell, SWT.BORDER);
statusLine.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
// 初始化图片列表
imageList = new ImageList();
// 窗口大小
shell.setBounds(100, 100, 800, 600);
shell.open();
while (!shell.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
// 清理资源
disposeAll();
display.dispose();
}
// 创建菜单栏(简化版)
private void createMenuBar() {
Menu menuBar = new Menu(shell, SWT.BAR);
shell.setMenuBar(menuBar);
// 文件菜单
MenuItem fileMenu = new MenuItem(menuBar, SWT.CASCADE);
fileMenu.setText("文件(&F)");
Menu fileMenuContent = new Menu(shell, SWT.DROP_DOWN);
fileMenu.setMenu(fileMenuContent);
MenuItem openItem = new MenuItem(fileMenuContent, SWT.PUSH);
openItem.setText("打开(&O)\tCtrl+O");
openItem.setAccelerator(SWT.MOD1 + 'O');
openItem.addListener(SWT.Selection, event -> openImage());
MenuItem saveItem = new MenuItem(fileMenuContent, SWT.PUSH);
saveItem.setText("保存(&S)\tCtrl+S");
saveItem.setAccelerator(SWT.MOD1 + 'S');
saveItem.addListener(SWT.Selection, event -> saveImage());
MenuItem exitItem = new MenuItem(fileMenuContent, SWT.PUSH);
exitItem.setText("退出(&X)\tCtrl+Q");
exitItem.setAccelerator(SWT.MOD1 + 'Q');
exitItem.addListener(SWT.Selection, event -> shell.dispose());
// 视图菜单
MenuItem viewMenu = new MenuItem(menuBar, SWT.CASCADE);
viewMenu.setText("视图(&V)");
Menu viewMenuContent = new Menu(shell, SWT.DROP_DOWN);
viewMenu.setMenu(viewMenuContent);
MenuItem zoomInItem = new MenuItem(viewMenuContent, SWT.PUSH);
zoomInItem.setText("放大(&+)\tCtrl++");
zoomInItem.setAccelerator(SWT.MOD1 + '+');
zoomInItem.addListener(SWT.Selection, event -> zoomIn());
MenuItem zoomOutItem = new MenuItem(viewMenuContent, SWT.PUSH);
zoomOutItem.setText("缩小(&-)\tCtrl+-");
zoomOutItem.setAccelerator(SWT.MOD1 + '-');
zoomOutItem.addListener(SWT.Selection, event -> zoomOut());
MenuItem fitToWindowItem = new MenuItem(viewMenuContent, SWT.PUSH);
fitToWindowItem.setText("适应窗口(&W)\tCtrl+0");
fitToWindowItem.setAccelerator(SWT.MOD1 + '0');
fitToWindowItem.addListener(SWT.Selection, event -> fitToWindow());
new MenuItem(viewMenuContent, SWT.SEPARATOR);
MenuItem fullScreenItem = new MenuItem(viewMenuContent, SWT.PUSH);
fullScreenItem.setText("全屏模式(&F11)");
fullScreenItem.setAccelerator(SWT.F11);
fullScreenItem.addListener(SWT.Selection, event -> toggleFullScreen());
// 帮助菜单
MenuItem helpMenu = new MenuItem(menuBar, SWT.CASCADE);
helpMenu.setText("帮助(&H)");
Menu helpMenuContent = new Menu(shell, SWT.DROP_DOWN);
helpMenu.setMenu(helpMenuContent);
MenuItem aboutItem = new MenuItem(helpMenuContent, SWT.PUSH);
aboutItem.setText("关于(&A)");
aboutItem.addListener(SWT.Selection, event -> showAbout());
}
// 创建工具栏
private void createToolBar() {
toolBar = new ToolBar(shell, SWT.FLAT | SWT.HORIZONTAL);
toolBar.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
// 打开按钮
ToolItem openToolItem = new ToolItem(toolBar, SWT.PUSH);
openToolItem.setText("打开");
openToolItem.setToolTipText("打开图片(Ctrl+O)");
openToolItem.addListener(SWT.Selection, event -> openImage());
// 上一张按钮
ToolItem previousToolItem = new ToolItem(toolBar, SWT.PUSH);
previousToolItem.setText("上一张");
previousToolItem.setToolTipText("上一张(左箭头)");
previousToolItem.addListener(SWT.Selection, event -> previousImage());
// 下一张按钮
ToolItem nextToolItem = new ToolItem(toolBar, SWT.PUSH);
nextToolItem.setText("下一张");
nextToolItem.setToolTipText("下一张(右箭头)");
nextToolItem.addListener(SWT.Selection, event -> nextImage());
new ToolItem(toolBar, SWT.SEPARATOR);
// 放大按钮
ToolItem zoomInToolItem = new ToolItem(toolBar, SWT.PUSH);
zoomInToolItem.setText("放大");
zoomInToolItem.setToolTipText("放大(Ctrl++)");
zoomInToolItem.addListener(SWT.Selection, event -> zoomIn());
// 缩小按钮
ToolItem zoomOutToolItem = new ToolItem(toolBar, SWT.PUSH);
zoomOutToolItem.setText("缩小");
zoomOutToolItem.setToolTipText("缩小(Ctrl+-)");
zoomOutToolItem.addListener(SWT.Selection, event -> zoomOut());
// 适应窗口按钮
ToolItem fitToWindowToolItem = new ToolItem(toolBar, SWT.PUSH);
fitToWindowToolItem.setText("适应");
fitToWindowToolItem.setToolTipText("适应窗口(Ctrl+0)");
fitToWindowToolItem.addListener(SWT.Selection, event -> fitToWindow());
new ToolItem(toolBar, SWT.SEPARATOR);
// 向左旋转按钮
ToolItem rotateLeftToolItem = new ToolItem(toolBar, SWT.PUSH);
rotateLeftToolItem.setText("左旋");
rotateLeftToolItem.setToolTipText("向左旋转 90 度");
rotateLeftToolItem.addListener(SWT.Selection, event -> rotateLeft());
// 向右旋转按钮
ToolItem rotateRightToolItem = new ToolItem(toolBar, SWT.PUSH);
rotateRightToolItem.setText("右旋");
rotateRightToolItem.setToolTipText("向右旋转 90 度");
rotateRightToolItem.addListener(SWT.Selection, event -> rotateRight());
new ToolItem(toolBar, SWT.SEPARATOR);
// 全屏按钮
ToolItem fullScreenToolItem = new ToolItem(toolBar, SWT.PUSH);
fullScreenToolItem.setText("全屏");
fullScreenToolItem.setToolTipText("全屏模式(F11)");
fullScreenToolItem.addListener(SWT.Selection, event -> toggleFullScreen());
// 删除按钮
ToolItem deleteToolItem = new ToolItem(toolBar, SWT.PUSH);
deleteToolItem.setText("删除");
deleteToolItem.setToolTipText("删除当前图片");
deleteToolItem.addListener(SWT.Selection, event -> deleteImage());
}
// 绘制画布
private void paintCanvas(GC gc) {
Rectangle clientArea = canvas.getClientArea();
// 绘制背景
gc.setBackground(display.getSystemColor(SWT.COLOR_BLACK));
gc.fillRectangle(clientArea);
// 绘制图片
if (currentImage != null && !currentImage.isDisposed()) {
ImageInfo imageInfo = imageList.getCurrentImage();
if (imageInfo != null) {
Rectangle bounds = calculateImageBounds(clientArea);
// 绘制旋转后的图片
drawRotatedImage(gc, currentImage,
bounds.x, bounds.y, bounds.width, bounds.height,
imageInfo.getRotation());
}
}
}
// 计算图片显示区域
private Rectangle calculateImageBounds(Rectangle clientArea) {
if (currentImage == null || currentImage.isDisposed()) {
return null;
}
int imageWidth = currentImage.getBounds().width;
int imageHeight = currentImage.getBounds().height;
// 应用缩放
double zoom = ZOOM_LEVELS[currentZoomLevel];
int scaledWidth = (int)(imageWidth * zoom);
int scaledHeight = (int)(imageHeight * zoom);
// 居中显示
int x = (clientArea.width - scaledWidth) / 2;
int y = (clientArea.height - scaledHeight) / 2;
return new Rectangle(x, y, scaledWidth, scaledHeight);
}
// 绘制旋转后的图片
private void drawRotatedImage(GC gc, Image image, int x, int y, int width, int height, int rotation) {
if (rotation == 0) {
gc.drawImage(image, x, y, width, height);
return;
}
// 旋转图片中心
Point center = new Point(x + width / 2, y + height / 2);
// 应用变换
Transform transform = new Transform(display);
transform.translate(center.x, center.y);
transform.rotate((float)Math.toRadians(rotation));
transform.translate(-center.x, -center.y);
gc.setTransform(transform);
gc.drawImage(image, x, y, width, height);
gc.setTransform(null);
}
// 打开图片
private void openImage() {
FileDialog fileDialog = new FileDialog(shell, SWT.OPEN);
fileDialog.setText("打开图片");
fileDialog.setFilterExtensions(new String[]{"*.jpg", "*.jpeg", "*.png", "*.gif", "*.bmp"});
fileDialog.setFilterNames(new String[]{
"JPEG 图片 (*.jpg, *.jpeg)",
"PNG 图片 (*.png)",
"GIF 图片 (*.gif)",
"BMP 图片 (*.bmp)"
});
String filePath = fileDialog.open();
if (filePath != null) {
openImage(filePath);
}
}
// 打开指定路径的图片
private void openImage(String filePath) {
try {
// 创建图片信息
ImageInfo imageInfo = new ImageInfo();
imageInfo.setFilePath(filePath);
File file = new File(filePath);
imageInfo.setFileName(file.getName());
imageInfo.setFormat(getFileFormat(file));
imageInfo.setFileSize(file.length());
imageInfo.setLastModified(file.lastModified());
// 加载图片
if (currentImage != null) {
currentImage.dispose();
}
currentImage = new Image(display, filePath);
ImageData imageData = currentImage.getImageData();
imageInfo.setWidth(imageData.width);
imageInfo.setHeight(imageData.height);
// 重置缩放和旋转
currentZoomLevel = 3; // 1.0 倍
rotation = 0; // 0 度
// 添加到图片列表
imageList.addImage(imageInfo);
// 更新 UI
canvas.redraw();
updateStatusBar();
} catch (Exception e) {
e.printStackTrace();
MessageBox messageBox = new MessageBox(shell, SWT.ICON_ERROR | SWT.OK);
messageBox.setText("错误");
messageBox.setMessage("无法打开图片:" + e.getMessage());
messageBox.open();
}
}
// 上一张图片
private void previousImage() {
ImageInfo imageInfo = imageList.getPreviousImage();
if (imageInfo != null) {
loadImage(imageInfo);
}
}
// 下一张图片
private void nextImage() {
ImageInfo imageInfo = imageList.getNextImage();
if (imageInfo != null) {
loadImage(imageInfo);
}
}
// 加载指定索引的图片
private void loadImage(ImageInfo imageInfo) {
if (imageInfo != null) {
if (currentImage != null && !currentImage.isDisposed()) {
currentImage.dispose();
}
try {
currentImage = new Image(display, imageInfo.getFilePath());
// 重置缩放和旋转
currentZoomLevel = 3; // 1.0 倍
rotation = 0; // 0 度
// 更新 UI
canvas.redraw();
updateStatusBar();
} catch (Exception e) {
e.printStackTrace();
MessageBox messageBox = new MessageBox(shell, SWT.ICON_ERROR | SWT.OK);
messageBox.setText("错误");
messageBox.setMessage("无法加载图片:" + e.getMessage());
messageBox.open();
}
}
}
// 放大
private void zoomIn() {
if (currentZoomLevel < ZOOM_LEVELS.length - 1) {
currentZoomLevel++;
canvas.redraw();
updateStatusBar();
}
}
// 缩小
private void zoomOut() {
if (currentZoomLevel > 0) {
currentZoomLevel--;
canvas.redraw();
updateStatusBar();
}
}
// 适应窗口
private void fitToWindow() {
Rectangle clientArea = canvas.getClientArea();
ImageInfo imageInfo = imageList.getCurrentImage();
if (imageInfo == null || currentImage == null || currentImage.isDisposed()) {
return;
}
int imageWidth = currentImage.getBounds().width;
int imageHeight = currentImage.getBounds().height;
// 找到最接近的缩放级别
double scaleX = (double) clientArea.width / imageWidth;
double scaleY = (clientArea.height - 100) / imageHeight; // 留下留 100 像素给状态栏
double scale = Math.min(scaleX, scaleY);
currentZoomLevel = 0;
for (int i = 0; i < ZOOM_LEVELS.length; i++) {
if (ZOOM_LEVELS[i] >= scale) {
currentZoomLevel = i;
break;
}
}
canvas.redraw();
updateStatusBar();
}
// 向左旋转
private void rotateLeft() {
rotation = (rotation + 270) % 360;
canvas.redraw();
updateStatusBar();
}
// 向右旋转
private void rotateRight() {
rotation = (rotation + 90) % 360;
canvas.redraw();
updateStatusBar();
}
// 切换全屏模式
private void toggleFullScreen() {
shell.setFullScreen(!shell.getFullScreen());
}
// 删除图片
private void deleteImage() {
ImageInfo currentImage = imageList.getCurrentImage();
if (currentImage == null) {
MessageBox messageBox = new MessageBox(shell, SWT.ICON_WARNING | SWT.OK);
messageBox.setText("警告");
messageBox.setMessage("没有图片可删除");
messageBox.open();
return;
}
// 询问确认
MessageBox messageBox = new MessageBox(shell, SWT.ICON_QUESTION | SWT.YES | SWT.NO);
messageBox.setText("删除");
messageBox.setMessage("确定要删除当前图片吗?");
int result = messageBox.open();
if (result == SWT.YES) {
// 从列表中删除
int index = imageList.getCurrentIndex();
imageList.remove(index);
// 释放当前图片
if (currentImage != null && !currentImage.isDisposed()) {
currentImage.dispose();
currentImage = null;
}
// 加载下一张图片
ImageInfo nextImage = imageList.getImage(index);
if (nextImage != null) {
loadImage(nextImage);
} else if (!imageList.isEmpty()) {
// 清空画布
canvas.redraw();
updateStatusBar();
}
}
}
// 保存图片
private void saveImage() {
ImageInfo currentImage = imageList.getCurrentImage();
if (currentImage == null) {
MessageBox messageBox = new MessageBox(shell, SWT.ICON_WARNING | SWT.OK);
messageBox.setText("警告");
messageBox.setMessage("没有图片可保存");
messageBox.open();
return;
}
FileDialog fileDialog = new FileDialog(shell, SWT.SAVE);
fileDialog.setText("保存图片");
fileDialog.setFileName("image_" + System.currentTimeMillis() + ".png");
fileDialog.setFilterExtensions(new String[]{"*.png", "*.jpg", "*.jpeg", "*.gif", "*.bmp"});
fileDialog.setFilterNames(new String[]{
"PNG 图片 (*.png)",
"JPEG 图片 (*.jpg, *.jpeg)",
"GIF 图片 (*.gif)",
"BMP 图片 (*.bmp)"
});
String filePath = fileDialog.open();
if (filePath != null) {
try {
// 保存图片
ImageLoader imageLoader = new ImageLoader();
imageLoader.load(currentImage.getFilePath());
// 如果图片有旋转或缩放,创建新图片
if (rotation != 0 || currentZoomLevel != 3) {
Image transformedImage = createTransformedImage();
imageLoader.save(filePath, SWT.IMAGE_PNG);
transformedImage.dispose();
} else {
imageLoader.save(filePath, SWT.IMAGE_PNG);
}
MessageBox messageBox = new MessageBox(shell, SWT.ICON_INFORMATION | SWT.OK);
messageBox.setText("成功");
messageBox.setMessage("图片已保存到:" + filePath);
messageBox.open();
} catch (Exception e) {
e.printStackTrace();
MessageBox messageBox = new MessageBox(shell, SWT.ICON_ERROR | SWT.OK);
messageBox.setText("错误");
messageBox.setMessage("无法保存图片:" + e.getMessage());
messageBox.open();
}
}
}
// 创建变换后的图片(用于保存旋转或缩放后的图片)
private Image createTransformedImage() {
if (currentImage == null || currentImage.isDisposed()) {
return null;
}
ImageData imageData = currentImage.getImageData();
Image transformedImage = new Image(display, imageData.width, imageData.height);
GC gc = new GC(transformedImageImage);
try {
// 计算变换后的图片
Rectangle clientArea = canvas.getClientArea();
Rectangle bounds = calculateImageBounds(clientArea);
if (bounds != null) {
drawRotatedImage(gc, currentImage, bounds.x, bounds.y,
bounds.width, bounds.height, rotation);
}
} finally {
gc.dispose();
}
return transformedImage;
}
// 更新状态栏
private void updateStatusBar() {
ImageInfo imageInfo = imageList.getCurrentImage();
if (imageInfo == null) {
statusLine.setMessage("没有图片");
return;
}
String status = String.format("%s | %s | %dx%d | 缩放: %.0f%% | 旋转: %d° | 第 %d/%d 张",
imageInfo.getFileName(),
imageInfo.getFormat(),
imageInfo.getWidth(),
ZOOM_LEVELS[currentZoomLevel] * 100,
rotation,
imageList.getCurrentIndex() + 1,
imageList.size());
statusLine.setMessage(status);
}
// 显示关于对话框
private void showAbout() {
MessageBox messageBox = new MessageBox(shell, SWT.ICON_INFORMATION | SWT.OK);
messageBox.setText("关于");
messageBox.setMessage("图片浏览器 v1.0\n\n一个简单的图片浏览器,\n支持打开、浏览、缩放、旋转图片。\n\n使用 SWT 构建。\n\n© 2024");
messageBox.open();
}
// 释放所有资源
private void disposeAll() {
if (currentImage != null && !currentImage.isDisposed()) {
currentImage.dispose();
}
currentImage = null;
imageList.clear();
}
// 获取文件格式
private String getFileFormat(File file) {
String name = file.getName().toLowerCase();
if (name.endsWith(".jpg") || name.endsWith(".jpeg")) {
return "JPG";
} else if (name.endsWith(".png")) {
return "PNG";
} else if (name.endsWith(".gif")) {
return "GIF";
} else if (name.endsWith(".bmp")) {
return "BMP";
}
return "Unknown";
}
}
19.6 本章小结
图片浏览器功能总结
| 功能 | 作用 | 类比 |
|---|---|---|
| 打开图片 | 从文件选择器加载图片 | 挂照片到墙上 |
| 图片浏览 | 上一张/下一张/轮播 | 播放幻灯片 |
| 缩放 | 放大/缩小/适应窗口 | 放大/缩小照片 |
| 旋转 | 向左/向右/翻转 | 旋转照片 |
| 全屏模式 | 沉浸式浏览 | 沉浸式观影 |
| 拖放 | 从文件管理器拖放 | 拖照片到墙上 |
费曼测试:你能解释清楚吗?
- 缩放图片的原理?
- 应用缩放比例到图片尺寸,计算显示位置和大小
- 旋转图片的原理?
- 使用变换矩阵旋转图片中心
- 全屏模式的作用?
- 隐藏窗口边框,提供沉浸式浏览体验
- 拖放打开的原理?
- 从文件管理器拖放文件到应用,自动打开
后记:SWT 的未来展望
技术的浪潮从未停止,
AWT、Swing、JavaFX 轮番登场,
而 SWT 依然坚守着"原生"的信念。
也许有一天会有新的选择,
但理解了 SWT,你就理解了
如何优雅地架起 Java 与系统之间的桥梁。
记住:
- 不重复造轮子:用系统提供的控件
- 尊重用户体验:让应用像本地应用
- 资源要管理:dispose() 别忘了
SWT 的本质:简单、快速、原生
完:SWT 教程
恭喜你完成了 SWT 教程的学习! 从入门到实战,从基础到高级, 你已经掌握了 SWT 的核心知识和实践技能。
学习回顾:
- 第 1-6 章:基础知识(Display、Shell、控件、布局、事件)
- 第 7-12 章:高级功能(表格、对话框、绘图、线程、资源管理、拖放)
- 第 13-17 章:系统集成与优化(系统调用、国际化、打包部署、性能优化)
- 第 18-19 章:实战项目(笔记本应用、图片浏览器)
下一步建议:
- 阅读 SWT 官方文档和 Javadoc
- 查看 Eclipse 源码,学习最佳实践
- 尝试开发自己的 SWT 应用
- 参与 SWT 社区,分享经验
最后的话:
编程不仅是技术,
更是艺术。
用代码创造价值,
用用户体验打动用户,
用美学打动心灵。
继续探索,继续创造,
用 SWT 构建美好的应用!
附录 A:常用 API 快速参考 (略,见官方文档)
附录 B:常见问题 FAQ (略,见官方文档)
附录 C:调试与错误排查 (略,见官方文档)
附录 D:扩展阅读
- SWT 官方文档:https://www.eclipse.org/swt/
- Eclipse Corner:https://www.eclipse.org/articles/
- SWT 社区:https://wiki.eclipse.org/SWT
祝愿:
愿你的 SWT 之旅顺利!
愿你的代码优雅高效!
愿你的应用深受用户喜爱!