第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() | 立即重绘 | 立即洗衣服 |
| 双缓冲 | 防止闪烁 | 备用黑板 |
费曼测试:你能解释清楚吗?
- GC 是什么?
- GC 是 Graphics Context,就像画笔,用于在画布(Canvas)上绘图
- redraw() 和 update() 的区别?
- redraw():标记需要重绘,异步处理 - update():立即重绘,同步处理
- 定时器的工作原理?
- 设定时间(如 100ms),时间到了执行任务,循环往复
- 双缓冲的作用?
- 防止动画闪烁,先在内存中绘制,再一次性绘制到屏幕
下一章预告
现在你已经掌握了绘图与动画,
可以画出任意图形,
但动画会阻塞 UI 线程,导致界面卡顿。
下一章,我们将学习线程与 SWT,
让动画在后台运行,
UI 始终流畅响应。
练习题:
- 创建一个自定义控件,绘制一个可以拖拽的矩形。
- 创建一个动画,让一个小球在画布上 bouncing(碰撞反弹)。
- 画一个时钟,显示当前时间,指针每秒转动。
(提示:拖拽使用 MouseListener 和 MouseMoveListener,动画使用 display.timerExec())