第19章:项目实战 - 图片浏览器

第19章:项目实战 - 图片浏览器


作者:步子哥 (steper@foxmail.com)


19.1 需求分析

功能列表

图片浏览器 = 电子相册
- 就像你在电脑上浏览照片
- 但可以在电脑上缩放、旋转、全屏浏览

类比:
图片浏览器 = 画展
- 画展上展出多张照片(图片列表)
- 你可以放大看细节(缩放)
- 你可以旋转看不同角度(旋转)
- 你可以全屏欣赏(全屏模式)

核心功能:

  1. 打开图片

- 从文件管理器拖放图片 - 通过文件对话框选择图片 - 支持多种格式(JPG、PNG、GIF、BMP)

  1. 图片浏览

- 上一张/下一张 - 幻动轮播 - 缩放(放大/缩小/适应窗口) - 旋转(向左/向右 90 度)

  1. 图像操作

- 保存图片 - 全屏模式 - 删除图片 - 图片信息显示

  1. 帮助功能

- 快捷键支持 - 工具栏 - 状态栏显示


技术选型

技术选型 = 工具选择
- 图片显示: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 本章小结

图片浏览器功能总结

功能作用类比
打开图片从文件选择器加载图片挂照片到墙上
图片浏览上一张/下一张/轮播播放幻灯片
缩放放大/缩小/适应窗口放大/缩小照片
旋转向左/向右/翻转旋转照片
全屏模式沉浸式浏览沉浸式观影
拖放从文件管理器拖放拖照片到墙上

费曼测试:你能解释清楚吗?

  1. 缩放图片的原理?

- 应用缩放比例到图片尺寸,计算显示位置和大小

  1. 旋转图片的原理?

- 使用变换矩阵旋转图片中心

  1. 全屏模式的作用?

- 隐藏窗口边框,提供沉浸式浏览体验

  1. 拖放打开的原理?

- 从文件管理器拖放文件到应用,自动打开

后记: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 之旅顺利!
愿你的代码优雅高效!
愿你的应用深受用户喜爱!
← 返回目录