第9章:绘图与动画 - Canvas 的艺术

第9章:绘图与动画 - Canvas 的艺术


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


9.1 GC(Graphics Context):画笔与画布

绘制线条、矩形、椭圆

Canvas canvas = new Canvas(shell, SWT.NONE);
canvas.addPaintListener(event -> {
    GC gc = event.gc;
    
    // 绘制线条
    gc.drawLine(10, 10, 200, 10);  // 起点 (10, 10),终点 (200, 10)
    
    // 绘制矩形
    gc.drawRectangle(10, 20, 100, 50);  // 起点 (10, 20),宽度 100,高度 50
    
    // 绘制椭圆
    gc.drawOval(10, 80, 100, 50);  // 起点 (10, 80),宽度 100,高度 50
});

费曼解释:GC 是什么?

GC = 画笔
- GC 是画笔
- Canvas 是画布
- 你用画笔在画布上画画

类比:
GC = 钢笔
- Canvas = 纸
- 你用钢笔在纸上写字、画画

颜色的设置

canvas.addPaintListener(event -> {
    GC gc = event.gc;
    
    // 设置前景色(线条、文字的颜色)
    gc.setForeground(display.getSystemColor(SWT.COLOR_RED));
    gc.drawLine(10, 10, 200, 10);
    
    // 设置背景色(填充区域)
    gc.setBackground(display.getSystemColor(SWT.COLOR_BLUE));
    gc.fillRectangle(10, 20, 100, 50);
    
    // 自定义颜色
    Color customColor = new Color(display, 255, 128, 0);  // RGB:255, 128, 0(橙色)
    gc.setBackground(customColor);
    gc.fillOval(10, 80, 100, 50);
    customColor.dispose();  // 别忘了释放
});

费曼解释:前景色和背景色的区别

前景色 = 笔的颜色
- 线条、文字的颜色
- 像画笔的墨水颜色

背景色 = 涂料的颜色
- 填充区域、背景的颜色
- 像油漆桶的油漆颜色

类比:
前景色 = 铅笔芯
- 铅笔芯的颜色(黑色、蓝色、红色...)
- 决定你画的线条颜色

背景色 = 粉笔
- 粉笔的颜色(白色、黄色、红色...)
- 决定你填充区域的颜色

字体的设置

canvas.addPaintListener(event -> {
    GC gc = event.gc;
    
    // 设置字体
    Font font = new Font(display, "Arial", 24, SWT.BOLD);
    gc.setFont(font);
    
    // 绘制文字
    gc.drawText("Hello, SWT!", 10, 10);
    
    // 获取文字的宽度和高度
    Point extent = gc.textExtent("Hello, SWT!");
    System.out.println("文字宽度:" + extent.x);
    System.out.println("文字高度:" + extent.y);
    
    font.dispose();  // 别忘了释放
});

费曼解释:字体的参数

Font(fontName, height, style)
- fontName:字体名称(Arial、Times New Roman...)
- height:字体大小(12、14、16...)
- style:字体样式(SWT.NORMAL、SWT.BOLD、SWT.ITALIC...)

类比:
Font = 字体设置
- 字体名称 = 你选择哪种字(楷书、行书、草书...)
- 字体大小 = 你写多大(1号字、2号字、3号字...)
- 字体样式 = 你怎么写(粗体、斜体、粗斜体...)

透明与混合

canvas.addPaintListener(event -> {
    GC gc = event.gc;
    
    // 设置透明度(0~255)
    gc.setAlpha(128);  // 50% 透明
    
    // 绘制半透明的矩形
    gc.setBackground(display.getSystemColor(SWT.COLOR_BLUE));
    gc.fillRectangle(10, 10, 100, 50);
    
    // 绘制半透明的椭圆
    gc.setBackground(display.getSystemColor(SWT.COLOR_RED));
    gc.fillOval(50, 30, 100, 50);
    
    // 恢复不透明
    gc.setAlpha(255);
    
    // 绘制不透明的文字
    gc.setForeground(display.getSystemColor(SWT.COLOR_BLACK));
    gc.drawText("透明效果", 10, 80);
});

费曼解释:透明度的含义

透明度 = 玻璃的透光度
- Alpha = 0:完全透明(像空气,看不见)
- Alpha = 255:完全不透明(像纸,看得见)
- Alpha = 128:半透明(像毛玻璃,若隐若现)

类比:
透明度 = 窗帘
- 完全拉开(Alpha = 0):窗帘完全透明,看得清外面
- 完全拉上(Alpha = 255):窗帘完全不透明,看不清外面
- 半拉开(Alpha = 128):窗帘半透明,若隐若现

代码示例:GC 绘图基础

public class GCDrawingExample {
    public static void main(String[] args) {
        Display display = new Display();
        Shell shell = new Shell(display);
        shell.setText("GC 绘图示例");
        shell.setLayout(new GridLayout(1, false));
        
        Canvas canvas = new Canvas(shell, SWT.BORDER);
        GridData canvasData = new GridData();
        canvasData.horizontalAlignment = GridData.FILL;
        canvasData.verticalAlignment = GridData.FILL;
        canvasData.grabExcessHorizontalSpace = true;
        canvasData.grabExcessVerticalSpace = true;
        canvasData.widthHint = 600;
        canvasData.heightHint = 400;
        canvas.setLayoutData(canvasData);
        
        canvas.addPaintListener(event -> {
            GC gc = event.gc;
            Rectangle clientArea = canvas.getClientArea();
            
            // 绘制背景
            gc.setBackground(display.getSystemColor(SWT.COLOR_WHITE));
            gc.fillRectangle(clientArea);
            
            // 绘制网格
            gc.setForeground(display.getSystemColor(SWT.COLOR_GRAY));
            gc.setLineWidth(1);
            for (int x = 0; x < clientArea.width; x += 50) {
                gc.drawLine(x, 0, x, clientArea.height);
            }
            for (int y = 0; y < clientArea.height; y += 50) {
                gc.drawLine(0, y, clientArea.width, y);
            }
            
            // 绘制线条
            gc.setForeground(display.getSystemColor(SWT.COLOR_BLACK));
            gc.setLineWidth(2);
            gc.drawLine(10, 10, 200, 10);
            gc.drawLine(10, 30, 200, 30);
            gc.drawLine(10, 50, 200, 50);
            
            // 绘制矩形
            gc.setForeground(display.getSystemColor(SWT.COLOR_RED));
            gc.drawRectangle(10, 70, 100, 50);
            
            gc.setBackground(display.getSystemColor(SWT.COLOR_RED));
            gc.fillRectangle(120, 70, 100, 50);
            
            // 绘制椭圆
            gc.setForeground(display.getSystemColor(SWT.COLOR_BLUE));
            gc.drawOval(10, 130, 100, 50);
            
            gc.setBackground(display.getSystemColor(SWT.COLOR_BLUE));
            gc.fillOval(120, 130, 100, 50);
            
            // 绘制圆角矩形
            gc.setForeground(display.getSystemColor(SWT.COLOR_GREEN));
            gc.drawRoundRectangle(240, 70, 100, 50, 20, 20);
            
            gc.setBackground(display.getSystemColor(SWT.COLOR_GREEN));
            gc.fillRoundRectangle(240, 130, 100, 50, 20, 20);
            
            // 绘制文字
            gc.setForeground(display.getSystemColor(SWT.COLOR_BLACK));
            gc.setFont(new Font(display, "Arial", 14, SWT.NORMAL));
            gc.drawText("Hello, SWT!", 10, 200);
            
            gc.setFont(new Font(display, "Arial", 18, SWT.BOLD));
            gc.drawText("粗体文字", 10, 230);
            
            gc.setFont(new Font(display, "Arial", 14, SWT.ITALIC));
            gc.drawText("斜体文字", 10, 260);
            
            // 透明效果
            gc.setAlpha(128);
            gc.setBackground(display.getSystemColor(SWT.COLOR_CYAN));
            gc.fillOval(200, 200, 100, 100);
            
            gc.setBackground(display.getSystemColor(SWT.COLOR_MAGENTA));
            gc.fillOval(250, 250, 100, 100);
            
            gc.setAlpha(255);
            
            // 绘制多边形
            gc.setForeground(display.getSystemColor(SWT.COLOR_DARK_BLUE));
            int[] points = {10, 300, 50, 350, 90, 300, 50, 250};
            gc.drawPolygon(points);
            
            gc.setBackground(display.getSystemColor(SWT.COLOR_DARK_BLUE));
            gc.fillPolygon(points);
        });
        
        shell.setBounds(100, 100, 700, 500);
        shell.open();
        
        while (!shell.isDisposed()) {
            if (!display.readAndDispatch()) {
                display.sleep();
            }
        }
        
        display.dispose();
    }
}

9.2 自定义控件入门

继承 Canvas 类

public class CircleControl extends Canvas {
    private int radius = 50;
    private Color color;
    
    public CircleControl(Composite parent, int style) {
        super(parent, style);
        
        // 初始化颜色
        color = new Color(getDisplay(), new RGB(255, 0, 0));
        
        // 添加绘图监听器
        addPaintListener(event -> {
            GC gc = event.gc;
            Rectangle clientArea = getClientArea();
            
            // 绘制背景
            gc.setBackground(getDisplay().getSystemColor(SWT.COLOR_WHITE));
            gc.fillRectangle(clientArea);
            
            // 绘制圆
            gc.setBackground(color);
            gc.fillOval(clientArea.width / 2 - radius, clientArea.height / 2 - radius, radius * 2, radius * 2);
        });
        
        // 添加释放监听器
        addDisposeListener(event -> {
            color.dispose();
        });
    }
    
    public void setRadius(int radius) {
        this.radius = radius;
        redraw();
    }
    
    public void setColor(Color color) {
        this.color.dispose();
        this.color = color;
        redraw();
    }
}

费曼解释:自定义控件的思想

自定义控件 = 自己制作玩具
- 你设计玩具的样子(绘图)
- 你制作玩具的功能(方法)
- 别人可以玩你的玩具(使用)

类比:
自定义控件 = 发明新工具
- 你设计工具的样子
- 你制作工具的功能
- 别人可以用你的工具

重写 paintControl 方法

public class CustomButton extends Canvas {
    private String text = "按钮";
    private boolean pressed = false;
    
    public CustomButton(Composite parent, int style) {
        super(parent, style);
        
        addPaintListener(event -> {
            paintControl(event.gc);
        });
        
        addMouseListener(new MouseAdapter() {
            @Override
            public void mouseDown(MouseEvent e) {
                pressed = true;
                redraw();
            }
            
            @Override
            public void mouseUp(MouseEvent e) {
                pressed = false;
                redraw();
                
                // 触发选择事件
                notifyListeners(SWT.Selection, new Event());
            }
        });
    }
    
    private void paintControl(GC gc) {
        Rectangle clientArea = getClientArea();
        
        // 绘制背景
        gc.setBackground(pressed ? display.getSystemColor(SWT.COLOR_DARK_GRAY) : display.getSystemColor(SWT.COLOR_GRAY));
        gc.fillRoundRectangle(0, 0, clientArea.width, clientArea.height, 10, 10);
        
        // 绘制边框
        gc.setForeground(display.getSystemColor(SWT.COLOR_BLACK));
        gc.setLineWidth(2);
        gc.drawRoundRectangle(0, 0, clientArea.width - 1, clientArea.height - 1, 10, 10);
        
        // 绘制文字
        gc.setForeground(display.getSystemColor(SWT.COLOR_WHITE));
        gc.setFont(new Font(display, "Arial", 12, SWT.BOLD));
        
        Point extent = gc.textExtent(text);
        int x = (clientArea.width - extent.x) / 2;
        int y = (clientArea.height - extent.y) / 2;
        gc.drawText(text, x, y, true);
    }
    
    public void setText(String text) {
        this.text = text;
        redraw();
    }
}

费曼解释:paintControl 方法的作用

paintControl = 绘画师
- 每次控件重绘
- 自动调用 paintControl
- 负责绘制控件的外观

类比:
paintControl = 画家
- 画布(控件)需要重画
- 画家(paintControl)拿起画笔
- 按照你的设计画一遍

响应鼠标事件

public class DraggableCircle extends Canvas {
    private int x = 100;
    private int y = 100;
    private int radius = 50;
    private boolean dragging = false;
    private int offsetX, offsetY;
    
    public DraggableCircle(Composite parent, int style) {
        super(parent, style);
        
        addPaintListener(event -> {
            paintControl(event.gc);
        });
        
        addMouseListener(new MouseAdapter() {
            @Override
            public void mouseDown(MouseEvent e) {
                // 检测是否点击了圆
                int dx = e.x - x;
                int dy = e.y - y;
                if (dx * dx + dy * dy <= radius * radius) {
                    dragging = true;
                    offsetX = dx;
                    offsetY = dy;
                }
            }
            
            @Override
            public void mouseUp(MouseEvent e) {
                dragging = false;
            }
        });
        
        addMouseMoveListener(event -> {
            if (dragging) {
                x = event.x - offsetX;
                y = event.y - offsetY;
                redraw();
            }
        });
    }
    
    private void paintControl(GC gc) {
        Rectangle clientArea = getClientArea();
        
        // 绘制背景
        gc.setBackground(getDisplay().getSystemColor(SWT.COLOR_WHITE));
        gc.fillRectangle(clientArea);
        
        // 绘制圆
        gc.setBackground(getDisplay().getSystemColor(SWT.COLOR_BLUE));
        gc.fillOval(x - radius, y - radius, radius * 2, radius * 2);
        
        // 绘制边框
        gc.setForeground(getDisplay().getSystemColor(SWT.COLOR_BLACK));
        gc.setLineWidth(2);
        gc.drawOval(x - radius, y - radius, radius * 2, radius * 2);
    }
}

费曼解释:拖拽的实现原理

拖拽 = 牵着风筝跑
- 你抓住风筝(鼠标按下)
- 你牵着风筝跑(鼠标移动)
- 你松开手,风筝飞走(鼠标释放)

类比:
拖拽 = 玩玩具车
- 手按住玩具车(鼠标按下)
- 手推着玩具车走(鼠标移动)
- 手松开,玩具车停下(鼠标释放)

代码示例:自定义控件

public class CustomControlExample {
    public static void main(String[] args) {
        Display display = new Display();
        Shell shell = new Shell(display);
        shell.setText("自定义控件示例");
        shell.setLayout(new GridLayout(1, false));
        
        // 自定义圆控件
        Label circleLabel = new Label(shell, SWT.NONE);
        circleLabel.setText("可拖拽的圆:");
        
        CircleControl circle = new CircleControl(shell, SWT.BORDER);
        GridData circleData = new GridData();
        circleData.horizontalAlignment = GridData.FILL;
        circleData.grabExcessHorizontalSpace = true;
        circleData.heightHint = 150;
        circle.setLayoutData(circleData);
        
        // 自定义按钮
        Label buttonLabel = new Label(shell, SWT.NONE);
        buttonLabel.setText("自定义按钮:");
        
        CustomButton customButton = new CustomButton(shell, SWT.BORDER);
        customButton.setText("点击我");
        customButton.addListener(SWT.Selection, event -> {
            System.out.println("自定义按钮被点击");
        });
        
        // 可拖拽的圆
        Label draggableLabel = new Label(shell, SWT.NONE);
        draggableLabel.setText("可拖拽的圆:");
        
        DraggableCircle draggableCircle = new DraggableCircle(shell, SWT.BORDER);
        GridData draggableData = new GridData();
        draggableData.horizontalAlignment = GridData.FILL;
        draggableData.grabExcessHorizontalSpace = true;
        draggableData.heightHint = 200;
        draggableCircle.setLayoutData(draggableData);
        
        shell.setBounds(100, 100, 600, 500);
        shell.open();
        
        while (!shell.isDisposed()) {
            if (!display.readAndDispatch()) {
                display.sleep();
            }
        }
        
        display.dispose();
    }
    
    // 自定义圆控件
    static class CircleControl extends Canvas {
        private int radius = 50;
        private Color color;
        
        public CircleControl(Composite parent, int style) {
            super(parent, style);
            
            color = new Color(getDisplay(), new RGB(255, 0, 0));
            
            addPaintListener(event -> {
                paintControl(event.gc);
            });
            
            addDisposeListener(event -> {
                color.dispose();
            });
        }
        
        private void paintControl(GC gc) {
            Rectangle clientArea = getClientArea();
            
            // 绘制背景
            gc.setBackground(getDisplay().getSystemColor(SWT.COLOR_WHITE));
            gc.fillRectangle(clientArea);
            
            // 绘制圆
            gc.setBackground(color);
            gc.fillOval(clientArea.width / 2 - radius, clientArea.height / 2 - radius, radius * 2, radius * 2);
            
            // 绘制边框
            gc.setForeground(getDisplay().getSystemColor(SWT.COLOR_BLACK));
            gc.setLineWidth(2);
            gc.drawOval(clientArea.width / 2 - radius, clientArea.height / 2 - radius, radius * 2, radius * 2);
        }
        
        public void setRadius(int radius) {
            this.radius = radius;
            redraw();
        }
        
        public void setColor(Color color) {
            this.color.dispose();
            this.color = color;
            redraw();
        }
    }
    
    // 自定义按钮
    static class CustomButton extends Canvas {
        private String text = "按钮";
        private boolean pressed = false;
        
        public CustomButton(Composite parent, int style) {
            super(parent, style);
            
            addPaintListener(event -> {
                paintControl(event.gc);
            });
            
            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseDown(MouseEvent e) {
                    pressed = true;
                    redraw();
                }
                
                @Override
                public void mouseUp(MouseEvent e) {
                    pressed = false;
                    redraw();
                    notifyListeners(SWT.Selection, new Event());
                }
            });
        }
        
        private void paintControl(GC gc) {
            Rectangle clientArea = getClientArea();
            Display display = getDisplay();
            
            // 绘制背景
            gc.setBackground(pressed ? display.getSystemColor(SWT.COLOR_DARK_GRAY) : display.getSystemColor(SWT.COLOR_GRAY));
            gc.fillRoundRectangle(0, 0, clientArea.width, clientArea.height, 10, 10);
            
            // 绘制边框
            gc.setForeground(display.getSystemColor(SWT.COLOR_BLACK));
            gc.setLineWidth(2);
            gc.drawRoundRectangle(0, 0, clientArea.width - 1, clientArea.height - 1, 10, 10);
            
            // 绘制文字
            gc.setForeground(display.getSystemColor(SWT.COLOR_WHITE));
            gc.setFont(new Font(display, "Arial", 12, SWT.BOLD));
            
            Point extent = gc.textExtent(text);
            int textX = (clientArea.width - extent.x) / 2;
            int textY = (clientArea.height - extent.y) / 2;
            gc.drawText(text, textX, textY, true);
        }
        
        public void setText(String text) {
            this.text = text;
            redraw();
        }
    }
    
    // 可拖拽的圆
    static class DraggableCircle extends Canvas {
        private int x = 100;
        private int y = 100;
        private int radius = 50;
        private boolean dragging = false;
        private int offsetX, offsetY;
        
        public DraggableCircle(Composite parent, int style) {
            super(parent, style);
            
            addPaintListener(event -> {
                paintControl(event.gc);
            });
            
            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseDown(MouseEvent e) {
                    int dx = e.x - x;
                    int dy = e.y - y;
                    if (dx * dx + dy * dy <= radius * radius) {
                        dragging = true;
                        offsetX = dx;
                        offsetY = dy;
                    }
                }
                
                @Override
                public void mouseUp(MouseEvent e) {
                    dragging = false;
                }
            });
            
            addMouseMoveListener(event -> {
                if (dragging) {
                    x = event.x - offsetX;
                    y = event.y - offsetY;
                    redraw();
                }
            });
        }
        
        private void paintControl(GC gc) {
            Rectangle clientArea = getClientArea();
            Display display = getDisplay();
            
            // 绘制背景
            gc.setBackground(display.getSystemColor(SWT.COLOR_WHITE));
            gc.fillRectangle(clientArea);
            
            // 绘制网格
            gc.setForeground(display.getSystemColor(SWT.COLOR_GRAY));
            gc.setLineWidth(1);
            for (int i = 0; i < clientArea.width; i += 20) {
                gc.drawLine(i, 0, i, clientArea.height);
            }
            for (int i = 0; i < clientArea.height; i += 20) {
                gc.drawLine(0, i, clientArea.width, i);
            }
            
            // 绘制圆
            gc.setBackground(display.getSystemColor(SWT.COLOR_BLUE));
            gc.fillOval(x - radius, y - radius, radius * 2, radius * 2);
            
            // 绘制边框
            gc.setForeground(display.getSystemColor(SWT.COLOR_BLACK));
            gc.setLineWidth(2);
            gc.drawOval(x - radius, y - radius, radius * 2, radius * 2);
        }
    }
}

9.3 动画的基础

定时器的使用

Canvas canvas = new Canvas(shell, SWT.NONE);
int angle = 0;

// 创建定时器
Runnable timer = new Runnable() {
    @Override
    public void run() {
        if (!canvas.isDisposed()) {
            // 更新角度
            angle = (angle + 10) % 360;
            
            // 重绘
            canvas.redraw();
            
            // 100ms 后再次执行
            display.timerExec(100, this);
        }
    }
};

// 启动定时器
display.timerExec(100, timer);

canvas.addPaintListener(event -> {
    GC gc = event.gc;
    
    // 绘制旋转的圆
    int x = 100 + (int)(50 * Math.cos(Math.toRadians(angle)));
    int y = 100 + (int)(50 * Math.sin(Math.toRadians(angle)));
    gc.setBackground(display.getSystemColor(SWT.COLOR_RED));
    gc.fillOval(x - 20, y - 20, 40, 40);
});

费曼解释:定时器的原理

定时器 = 闹钟
- 设定时间(100ms)
- 时间到了,响铃(执行任务)
- 再次设定时间(100ms)
- 循环往复

类比:
定时器 = 节拍器
- 设定节拍(100ms 一次)
- 每次节拍,敲一下(执行任务)
- 循环往复,形成节奏

重绘机制(redraw() vs update())

// redraw():标记控件需要重绘
canvas.redraw();

// update():立即重绘
canvas.update();

// 组合使用
canvas.redraw();  // 标记需要重绘
canvas.update();  // 立即重绘

费曼解释:redraw() 和 update() 的区别

redraw() = 记笔记
- 记下"需要重绘"
- 等待事件循环处理
- 异步重绘

update() = 立即重绘
- 立即重绘
- 不等待事件循环
- 同步重绘

类比:
redraw() = 写备忘录
- 记下"需要洗衣服"
- 等待有空时再洗
- 异步处理

update() = 立即洗衣服
- 立即洗衣服
- 不等待
- 同步处理

双缓冲技术

canvas.addPaintListener(event -> {
    GC gc = event.gc;
    Rectangle clientArea = canvas.getClientArea();
    
    // 创建离屏图像
    Image offscreenImage = new Image(display, clientArea.width, clientArea.height);
    GC offscreenGC = new GC(offscreenImage);
    
    try {
        // 在离屏图像上绘制
        offscreenGC.setBackground(display.getSystemColor(SWT.COLOR_WHITE));
        offscreenGC.fillRectangle(clientArea);
        
        // 绘制复杂图形
        for (int i = 0; i < 100; i++) {
            offscreenGC.setBackground(new Color(display, new RGB(
                random.nextInt(256), random.nextInt(256), random.nextInt(256))));
            offscreenGC.fillOval(
                random.nextInt(clientArea.width),
                random.nextInt(clientArea.height),
                20, 20);
        }
        
        // 将离屏图像绘制到屏幕
        gc.drawImage(offscreenImage, 0, 0);
    } finally {
        offscreenGC.dispose();
        offscreenImage.dispose();
    }
});

费曼解释:双缓冲的原理

双缓冲 = 备用黑板
- 主黑板(屏幕):学生看到的
- 副黑板(内存):你在上面画画
- 画完后,把副黑板的内容复制到主黑板
- 学生看到的是完整的画面

类比:
双缓冲 = 舞台表演
- 前台(屏幕):观众看到的
- 后台(内存):演员准备的
- 准备好后,演员走到前台
- 观众看到的是完整的表演

代码示例:动画基础

public class AnimationExample {
    public static void main(String[] args) {
        Display display = new Display();
        Shell shell = new Shell(display);
        shell.setText("动画示例");
        shell.setLayout(new GridLayout(1, false));
        
        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;
        canvasData.widthHint = 600;
        canvasData.heightHint = 400;
        canvas.setLayoutData(canvasData);
        
        // 动画状态
        int ballX = 100;
        int ballY = 100;
        int ballRadius = 20;
        int ballVX = 5;
        int ballVY = 5;
        
        // 定时器
        Runnable timer = new Runnable() {
            @Override
            public void run() {
                if (!canvas.isDisposed()) {
                    // 更新小球位置
                    Rectangle clientArea = canvas.getClientArea();
                    ballX += ballVX;
                    ballY += ballVY;
                    
                    // 碰撞检测
                    if (ballX - ballRadius < 0 || ballX + ballRadius > clientArea.width) {
                        ballVX = -ballVX;
                    }
                    if (ballY - ballRadius < 0 || ballY + ballRadius > clientArea.height) {
                        ballVY = -ballVY;
                    }
                    
                    // 重绘
                    canvas.redraw();
                    
                    // 16ms 后再次执行(约 60 FPS)
                    display.timerExec(16, this);
                }
            }
        };
        
        // 绘图监听器
        canvas.addPaintListener(event -> {
            GC gc = event.gc;
            Rectangle clientArea = canvas.getClientArea();
            
            // 绘制背景
            gc.setBackground(display.getSystemColor(SWT.COLOR_WHITE));
            gc.fillRectangle(clientArea);
            
            // 绘制网格
            gc.setForeground(display.getSystemColor(SWT.COLOR_GRAY));
            gc.setLineWidth(1);
            for (int x = 0; x < clientArea.width; x += 20) {
                gc.drawLine(x, 0, x, clientArea.height);
            }
            for (int y = 0; y < clientArea.height; y += 20) {
                gc.drawLine(0, y, clientArea.width, y);
            }
            
            // 绘制小球
            gc.setBackground(display.getSystemColor(SWT.COLOR_RED));
            gc.fillOval(ballX - ballRadius, ballY - ballRadius, ballRadius * 2, ballRadius * 2);
            
            // 绘制小球边框
            gc.setForeground(display.getSystemColor(SWT.COLOR_BLACK));
            gc.setLineWidth(2);
            gc.drawOval(ballX - ballRadius, ballY - ballRadius, ballRadius * 2, ballRadius * 2);
        });
        
        // 按钮面板
        Composite buttonPanel = new Composite(shell, SWT.NONE);
        buttonPanel.setLayout(new FillLayout(SWT.HORIZONTAL));
        
        // 启动按钮
        Button startButton = new Button(buttonPanel, SWT.PUSH);
        startButton.setText("启动动画");
        startButton.addListener(SWT.Selection, event -> {
            display.timerExec(16, timer);
        });
        
        // 停止按钮
        Button stopButton = new Button(buttonPanel, SWT.PUSH);
        stopButton.setText("停止动画");
        stopButton.addListener(SWT.Selection, event -> {
            // 停止动画(通过重写 timer)
            // 实际应用中需要使用标志位控制
        });
        
        // 重置按钮
        Button resetButton = new Button(buttonPanel, SWT.PUSH);
        resetButton.setText("重置");
        resetButton.addListener(SWT.Selection, event -> {
            ballX = 100;
            ballY = 100;
            ballVX = 5;
            ballVY = 5;
            canvas.redraw();
        });
        
        shell.setBounds(100, 100, 700, 500);
        shell.open();
        
        while (!shell.isDisposed()) {
            if (!display.readAndDispatch()) {
                display.sleep();
            }
        }
        
        display.dispose();
    }
}

9.4 实战案例:画一个时钟

绘制表盘、刻度、指针

public class ClockCanvas extends Canvas {
    public ClockCanvas(Composite parent, int style) {
        super(parent, style);
        
        addPaintListener(event -> {
            paintClock(event.gc);
        });
        
        // 启动定时器
        Runnable timer = new Runnable() {
            @Override
            public void run() {
                if (!isDisposed()) {
                    redraw();
                    Display.getCurrent().timerExec(1000, this);  // 每秒更新一次
                }
            }
        };
        getDisplay().timerExec(1000, timer);
    }
    
    private void paintClock(GC gc) {
        Rectangle clientArea = getClientArea();
        int centerX = clientArea.width / 2;
        int centerY = clientArea.height / 2;
        int radius = Math.min(centerX, centerY) - 20;
        
        Display display = getDisplay();
        
        // 绘制表盘背景
        gc.setBackground(display.getSystemColor(SWT.COLOR_WHITE));
        gc.fillOval(centerX - radius, centerY - radius, radius * 2, radius * 2);
        
        // 绘制表盘边框
        gc.setForeground(display.getSystemColor(SWT.COLOR_BLACK));
        gc.setLineWidth(3);
        gc.drawOval(centerX - radius, centerY - radius, radius * 2, radius * 2);
        
        // 绘制刻度
        for (int i = 0; i < 60; i++) {
            int angle = i * 6;  // 每分钟 6 度
            boolean isHour = (i % 5 == 0);
            
            int innerRadius = isHour ? radius - 20 : radius - 10;
            int outerRadius = radius - 5;
            
            double radian = Math.toRadians(angle - 90);
            int x1 = centerX + (int)(innerRadius * Math.cos(radian));
            int y1 = centerY + (int)(innerRadius * Math.sin(radian));
            int x2 = centerX + (int)(outerRadius * Math.cos(radian));
            int y2 = centerY + (int)(outerRadius * Math.sin(radian));
            
            gc.setForeground(isHour ? display.getSystemColor(SWT.COLOR_BLACK) : display.getSystemColor(SWT.COLOR_GRAY));
            gc.setLineWidth(isHour ? 3 : 1);
            gc.drawLine(x1, y1, x2, y2);
        }
        
        // 绘制数字
        gc.setForeground(display.getSystemColor(SWT.COLOR_BLACK));
        gc.setFont(new Font(display, "Arial", 12, SWT.BOLD));
        for (int i = 1; i <= 12; i++) {
            int angle = i * 30;  // 每小时 30 度
            double radian = Math.toRadians(angle - 90);
            int x = centerX + (int)((radius - 35) * Math.cos(radian));
            int y = centerY + (int)((radius - 35) * Math.sin(radian));
            
            String number = String.valueOf(i);
            Point extent = gc.textExtent(number);
            gc.drawText(number, x - extent.x / 2, y - extent.y / 2, true);
        }
        
        // 获取当前时间
        Calendar calendar = Calendar.getInstance();
        int hour = calendar.get(Calendar.HOUR);
        int minute = calendar.get(Calendar.MINUTE);
        int second = calendar.get(Calendar.SECOND);
        
        // 绘制时针
        int hourAngle = (hour % 12) * 30 + minute / 2;  // 时针每小时 30 度,每分钟 0.5 度
        double hourRadian = Math.toRadians(hourAngle - 90);
        int hourLength = radius * 0.5;
        int hourX = centerX + (int)(hourLength * Math.cos(hourRadian));
        int hourY = centerY + (int)(hourLength * Math.sin(hourRadian));
        
        gc.setForeground(display.getSystemColor(SWT.COLOR_BLACK));
        gc.setLineWidth(6);
        gc.drawLine(centerX, centerY, hourX, hourY);
        
        // 绘制分针
        int minuteAngle = minute * 6;  // 分针每分钟 6 度
        double minuteRadian = Math.toRadians(minuteAngle - 90);
        int minuteLength = radius * 0.7;
        int minuteX = centerX + (int)(minuteLength * Math.cos(minuteRadian));
        int minuteY = centerY + (int)(minuteLength * Math.sin(minuteRadian));
        
        gc.setForeground(display.getSystemColor(SWT.COLOR_DARK_BLUE));
        gc.setLineWidth(4);
        gc.drawLine(centerX, centerY, minuteX, minuteY);
        
        // 绘制秒针
        int secondAngle = second * 6;  // 秒针每秒 6 度
        double secondRadian = Math.toRadians(secondAngle - 90);
        int secondLength = radius * 0.8;
        int secondX = centerX + (int)(secondLength * Math.cos(secondRadian));
        int secondY = centerY + (int)(secondLength * Math.sin(secondRadian));
        
        gc.setForeground(display.getSystemColor(SWT.COLOR_RED));
        gc.setLineWidth(2);
        gc.drawLine(centerX, centerY, secondX, secondY);
        
        // 绘制中心圆点
        gc.setBackground(display.getSystemColor(SWT.COLOR_BLACK));
        gc.fillOval(centerX - 5, centerY - 5, 10, 10);
    }
}

费曼解释:时钟的绘制原理

时钟 = 时间的可视化
- 表盘 = 时间容器(12 小时)
- 刻度 = 时间标记(60 分钟)
- 指针 = 时间指示(时、分、秒)

时针 = 粗指针
- 每小时走一格(30 度)
- 每分钟走 0.5 度

分针 = 中指针
- 每分钟走一格(6 度)
- 每小时走一圈(360 度)

秒针 = 细指针
- 每秒走一格(6 度)
- 每分钟走一圈(360 度)

类比:
时钟 = 日历
- 表盘 = 日历页(12 个月)
- 刻度 = 日期(30 天)
- 指针 = 今天(哪一天)

代码示例:完整时钟

public class ClockExample {
    public static void main(String[] args) {
        Display display = new Display();
        Shell shell = new Shell(display);
        shell.setText("时钟示例");
        shell.setLayout(new GridLayout(1, false));
        
        ClockCanvas clock = new ClockCanvas(shell, SWT.BORDER);
        GridData clockData = new GridData();
        clockData.horizontalAlignment = GridData.FILL;
        clockData.verticalAlignment = GridData.FILL;
        clockData.grabExcessHorizontalSpace = true;
        clockData.grabExcessVerticalSpace = true;
        clockData.widthHint = 400;
        clockData.heightHint = 400;
        clock.setLayoutData(clockData);
        
        // 显示数字时间
        Label timeLabel = new Label(shell, SWT.CENTER);
        timeLabel.setFont(new Font(display, "Arial", 24, SWT.BOLD));
        
        // 更新数字时间
        Runnable timer = new Runnable() {
            @Override
            public void run() {
                if (!shell.isDisposed()) {
                    Calendar calendar = Calendar.getInstance();
                    int hour = calendar.get(Calendar.HOUR_OF_DAY);
                    int minute = calendar.get(Calendar.MINUTE);
                    int second = calendar.get(Calendar.SECOND);
                    String timeText = String.format("%02d:%02d:%02d", hour, minute, second);
                    timeLabel.setText(timeText);
                    display.timerExec(100, this);
                }
            }
        };
        display.timerExec(100, timer);
        
        shell.setBounds(100, 100, 450, 500);
        shell.open();
        
        while (!shell.isDisposed()) {
            if (!display.readAndDispatch()) {
                display.sleep();
            }
        }
        
        display.dispose();
    }
    
    static class ClockCanvas extends Canvas {
        public ClockCanvas(Composite parent, int style) {
            super(parent, style);
            
            addPaintListener(event -> {
                paintClock(event.gc);
            });
            
            Runnable timer = new Runnable() {
                @Override
                public void run() {
                    if (!isDisposed()) {
                        redraw();
                        Display.getCurrent().timerExec(1000, this);
                    }
                }
            };
            getDisplay().timerExec(1000, timer);
        }
        
        private void paintClock(GC gc) {
            Rectangle clientArea = getClientArea();
            int centerX = clientArea.width / 2;
            int centerY = clientArea.height / 2;
            int radius = Math.min(centerX, centerY) - 20;
            
            Display display = getDisplay();
            
            // 绘制表盘
            gc.setBackground(display.getSystemColor(SWT.COLOR_WHITE));
            gc.fillOval(centerX - radius, centerY - radius, radius * 2, radius * 2);
            
            gc.setForeground(display.getSystemColor(SWT.COLOR_BLACK));
            gc.setLineWidth(3);
            gc.drawOval(centerX - radius, centerY - radius, radius * 2, radius * 2);
            
            // 绘制刻度
            for (int i = 0; i < 60; i++) {
                int angle = i * 6;
                boolean isHour = (i % 5 == 0);
                
                int innerRadius = isHour ? radius - 20 : radius - 10;
                int outerRadius = radius - 5;
                
                double radian = Math.toRadians(angle - 90);
                int x1 = centerX + (int)(innerRadius * Math.cos(radian));
                int y1 = centerY + (int)(innerRadius * Math.sin(radian));
                int x2 = centerX + (int)(outerRadius * Math.cos(radian));
                int y2 = centerY + (int)(outerRadius * Math.sin(radian));
                
                gc.setForeground(isHour ? display.getSystemColor(SWT.COLOR_BLACK) : display.getSystemColor(SWT.COLOR_GRAY));
                gc.setLineWidth(isHour ? 3 : 1);
                gc.drawLine(x1, y1, x2, y2);
            }
            
            // 绘制数字
            gc.setForeground(display.getSystemColor(SWT.COLOR_BLACK));
            gc.setFont(new Font(display, "Arial", 12, SWT.BOLD));
            for (int i = 1; i <= 12; i++) {
                int angle = i * 30;
                double radian = Math.toRadians(angle - 90);
                int x = centerX + (int)((radius - 35) * Math.cos(radian));
                int y = centerY + (int)((radius - 35) * Math.sin(radian));
                
                String number = String.valueOf(i);
                Point extent = gc.textExtent(number);
                gc.drawText(number, x - extent.x / 2, y - extent.y / 2, true);
            }
            
            // 获取当前时间
            Calendar calendar = Calendar.getInstance();
            int hour = calendar.get(Calendar.HOUR);
            int minute = calendar.get(Calendar.MINUTE);
            int second = calendar.get(Calendar.SECOND);
            
            // 时针
            int hourAngle = (hour % 12) * 30 + minute / 2;
            double hourRadian = Math.toRadians(hourAngle - 90);
            int hourLength = radius * 0.5;
            int hourX = centerX + (int)(hourLength * Math.cos(hourRadian));
            int hourY = centerY + (int)(hourLength * Math.sin(hourRadian));
            
            gc.setForeground(display.getSystemColor(SWT.COLOR_BLACK));
            gc.setLineWidth(6);
            gc.drawLine(centerX, centerY, hourX, hourY);
            
            // 分针
            int minuteAngle = minute * 6;
            double minuteRadian = Math.toRadians(minuteAngle - 90);
            int minuteLength = radius * 0.7;
            int minuteX = centerX + (int)(minuteLength * Math.cos(minuteRadian));
            int minuteY = centerY + (int)(minuteLength * Math.sin(minuteRadian));
            
            gc.setForeground(display.getSystemColor(SWT.COLOR_DARK_BLUE));
            gc.setLineWidth(4);
            gc.drawLine(centerX, centerY, minuteX, minuteY);
            
            // 秒针
            int secondAngle = second * 6;
            double secondRadian = Math.toRadians(secondAngle - 90);
            int secondLength = radius * 0.8;
            int secondX = centerX + (int)(secondLength * Math.cos(secondRadian));
            int secondY = centerY + (int)(secondLength * Math.sin(secondRadian));
            
            gc.setForeground(display.getSystemColor(SWT.COLOR_RED));
            gc.setLineWidth(2);
            gc.drawLine(centerX, centerY, secondX, secondY);
            
            // 中心圆点
            gc.setBackground(display.getSystemColor(SWT.COLOR_BLACK));
            gc.fillOval(centerX - 5, centerY - 5, 10, 10);
        }
    }
}

9.5 本章小结

绘图与动画总结

概念作用类比
GC绘图上下文画笔
Canvas画布
paintControl绘图方法画家
定时器动画触发闹钟
redraw()标记重绘写备忘录
update()立即重绘立即洗衣服
双缓冲防止闪烁备用黑板

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

  1. GC 是什么?

- GC 是 Graphics Context,就像画笔,用于在画布(Canvas)上绘图

  1. redraw() 和 update() 的区别?

- redraw():标记需要重绘,异步处理 - update():立即重绘,同步处理

  1. 定时器的工作原理?

- 设定时间(如 100ms),时间到了执行任务,循环往复

  1. 双缓冲的作用?

- 防止动画闪烁,先在内存中绘制,再一次性绘制到屏幕

下一章预告

现在你已经掌握了绘图与动画,
可以画出任意图形,
但动画会阻塞 UI 线程,导致界面卡顿。

下一章,我们将学习线程与 SWT,
让动画在后台运行,
UI 始终流畅响应。

练习题:

  1. 创建一个自定义控件,绘制一个可以拖拽的矩形。
  2. 创建一个动画,让一个小球在画布上 bouncing(碰撞反弹)。
  3. 画一个时钟,显示当前时间,指针每秒转动。

(提示:拖拽使用 MouseListener 和 MouseMoveListener,动画使用 display.timerExec())

← 返回目录