快速浏览一下您的代码,看起来在使用绘图命令、编写类以及它们如何交互方面存在一些混淆。
我会开始非常基础:
- 将所有内容拆分为简单易懂的独立任务
- 独立实施每个任务并对其进行测试
- 根据需要简化和概括代码
- 开始将实施整合到一个主项目中,逐个测试每个步骤。
例如,让我们来完成使用鼠标交互式绘制矩形的任务。如果存储了鼠标按下时的位置,可以将它们与坐标的差取到最近的鼠标坐标:矩形的尺寸:
//an object to store current mouse coordiates
PVector mouse = new PVector();
//...and the previous mouse coordinates
PVector pmouse = new PVector();
void setup(){
size(400,400);
}
void draw(){
background(255);
//compute width,height as difference between current and previous mouse positions
float w = mouse.x - pmouse.x;
float h = mouse.y - pmouse.y;
//draw the shape according to it's mode
rect(pmouse.x,pmouse.y,w,h);
}
//set both previous and current mouse coordinates - this helps reset coordinates and finalize a shape
void mouseSet(){
pmouse.set(mouseX,mouseY);
mouse.set(mouseX,mouseY);
}
void mousePressed(){
mouseSet();
}
void mouseDragged(){//update only the current mouse position, leaving pmouse outdated
mouse.set(mouseX,mouseY);
}
void mouseReleased(){
mouseSet();
}
如果您想渲染形状和形状本身的预览,使用图层(如在 Photoshop 中)可能会很方便:一个图层渲染已绘制的内容,另一个临时在顶部渲染预览。
幸运的是,在 Processing via PGraphics 中有类似的东西。
一旦你初始化了一个PGraphics 实例,你就可以使用你习惯的相同的绘图命令来绘制它。唯一的问题是您需要先调用beginDraw(),然后再调用endDraw()。
PGraphics 的另一个很酷的地方是它扩展了PImage,这意味着您可以将其显示为一个(更不用说,进行图像处理):
PGraphics canvas;
size(400,400);
//create a PGrahpics layer
canvas = createGraphics(width,height);
//intialize drawing
canvas.beginDraw();
//draw something, pretty similar you'd draw in Processing
canvas.background(255);
canvas.rect(200,200,150,100);
//finish drawing
canvas.endDraw();
//PGraphics extends PImage, hence it can drawn as one
image(canvas,0,0);
在您的代码中,按钮类保留对矩形和其他对象的引用。
理想情况下,您希望对象为loosely coupled。这个想法是让类处理自己的功能,独立于主程序或其他类。尝试将您的 Button 类复制到新草图中并立即使用。目前,您还需要复制其他不相关的类以进行编译。目标是编写一个可以在任何草图中轻松重用的 Button 类。
回到主要功能,您需要:
- 一种绘图形状模式(矩形或椭圆)
- 绘图颜色(来自颜色列表)
这意味着有多种形状和多种颜色,但每次只使用一种。将上述成分放在一起,如果更容易的话,您可以在没有 GUI 或类的情况下对功能进行原型设计。只需使用键盘快捷键替换此测试阶段的 GUI 按钮(使用 1/2/3 控制颜色,r 代表矩形,c 代表圆形):
PGraphics canvas;//a layer to persist shapes onto
//shape modes
int MODE_RECTANGLE = 0;
int MODE_ELLIPSE = 1;
//a reference to the currently selected shape mode
int mode = MODE_RECTANGLE;
//various colours
color c1 = color(192,0,0);
color c2 = color(225,225,0);
color c3 = color(0,0,192);
//a reference to the currently selected colour
color current = c1;
//an object to store current mouse coordiates
PVector mouse = new PVector();
//...and the previous mouse coordinates
PVector pmouse = new PVector();
void setup(){
size(400,400);
//setup ellipse mode to draw from corner like the rect()'s default setting
ellipseMode(CORNER);
strokeWeight(3);
//initialise the canvas - this allows you to draw with the same commands, but as a separate layer
canvas = createGraphics(width,height);
canvas.beginDraw();
//replicate ellipse mode and stroke weight in canvas layer as well (so what's being drawn matches preview)
canvas.ellipseMode(CORNER);
canvas.strokeWeight(3);
canvas.background(255);
canvas.endDraw();
}
void draw(){
//draw the layer first
image(canvas,0,0);
//overlay the preview on top using 50% transparency (as a visual hint it's a preview)
draw(g,127);
}
//a function that draws into a PGraphics layer (be it our canvas or Processing's)
void draw(PGraphics g,int transparency){
g.fill(current,transparency);
//compute width,height as difference between current and previous mouse positions
float w = mouse.x - pmouse.x;
float h = mouse.y - pmouse.y;
//draw the shape according to it's mode
if(mode == MODE_ELLIPSE) {
g.ellipse(pmouse.x,pmouse.y,w,h);
}
if(mode == MODE_RECTANGLE) {
g.rect(pmouse.x,pmouse.y,w,h);
}
}
//set both previous and current mouse coordinates - this helps reset coordinates and finalize a shape
void mouseSet(){
pmouse.set(mouseX,mouseY);
mouse.set(mouseX,mouseY);
}
void mousePressed(){
mouseSet();
}
void mouseDragged(){//update only the current mouse position, leaving pmouse outdated
mouse.set(mouseX,mouseY);
}
void mouseReleased(){
//commit the shape to the canvas layer
canvas.beginDraw();
draw(canvas,255);
canvas.endDraw();
//set both mouse positions
mouseSet();
}
//use keys to test: 1,2,3 = colours, r/c = shape mode
void keyPressed(){
if(key == '1') current = c1;
if(key == '2') current = c2;
if(key == '3') current = c3;
if(key == 'r') mode = MODE_RECTANGLE;
if(key == 'c') mode = MODE_ELLIPSE;
}
还记得我们之前是如何使用 PGraphics 的吗?每个处理小程序已经有一个,命名为g,因此这是一种使用单个函数绘制多个 PGraphics 实例(Processing 和我们的canvas)但每个具有不同透明度的快速而肮脏的方法。
使用这种方法值得注意的是,我们不是为每个绘制的形状存储多个圆形/矩形实例,而是简单地在画布中渲染一次并让它(一个PGraphics)对象存储像素。缺点是一旦绘制完成,您将无法检索以何种顺序以及以何种坐标/尺寸绘制的形状,但如果您不需要这些细节,它会更简单。
如果您确实需要这些,可能值得一试PShape(尤其是GROUP,RECT,ELLIPSE 选项)。
现在让我们添加按钮! Anthony 的建议很棒,简化的 buttonClicked 建议非常适合处理。
通常你会使用Interface 为按钮定义一个侦听器并让处理草图实现这个接口,但只需一个函数负责处理按钮,这是一个很好的解决方法。
这是一个基本的实现:
Button a = new Button("Button A",5,5,90,20,color(200),color(0));
Button b = new Button("Button B",5,30,90,20,color(200),color(0));
void draw(){
background(255);
//update button states based on mouse interaction
a.update(mouseX,mouseY,mousePressed);
b.update(mouseX,mouseY,mousePressed);
//render buttons on screen
a.draw();
b.draw();
}
void onButtonClicked(Button btn){
println(btn.label + " was pressed");
}
class Button{
float w,h,x,y;//width, height and position
color bg = color(200);//background colour
color fg = color(0);//foreground colour
String label;//text displayed
boolean isOver,wasPressed;//button states
int pw = 10;//padding on width
boolean outline;//draw an outline or not
Button(String label,float x,float y,float w,float h,color fg,color bg){
this.x = x;
this.y = y;
this.w = w;
this.h = h;
this.label = label;
this.fg = fg;
this.bg = bg;
}
void update(int mx,int my,boolean md){
//check bounding box
isOver = ((mx >= x && mx <= (x+w))&&(my >= y && my <= (y+h)));
if(isOver && md){
//check if it was not previously pressed to call the onButtonClicked function only once (similar to debouncing)
if(!wasPressed){
onButtonClicked(this);
wasPressed = true;
}
}else wasPressed = false;
}
void draw(){
//pushStyle()/popStyle() isolates drawing styles (similar to how pushMatrix()/popMatrix() isolates coordinate transformations
pushStyle();
if(outline){
strokeWeight(3);
stroke(127);
}else{
noStroke();
}
fill(isOver ? fg : bg);//the ? : is a lazy one liner way of doing if/else: (booleanExpression ? doIfTrue : doIfFalse)
rect(x,y,w,h);
fill(isOver ? bg : fg);
text(label,x+pw,y+h*.75);
popStyle();
}
}
现在只需使用键盘快捷键将这个 Button 类添加到前面的代码中就相当简单了:
PGraphics canvas;//a layer to persist shapes onto
//shape modes
int MODE_RECTANGLE = 0;
int MODE_ELLIPSE = 1;
//a reference to the currently selected shape mode
int mode = MODE_RECTANGLE;
//various colours
color c1 = color(192,0,0);
color c2 = color(225,225,0);
color c3 = color(0,0,192);
//a reference to the currently selected colour
color current = c1;
//an object to store current mouse coordiates
PVector mouse = new PVector();
//...and the previous mouse coordinates
PVector pmouse = new PVector();
//UI
//Button constructor: label, x,y, width, height, foreground, background
Button rectMode = new Button("\u25A0",5,5,30,20,color(200),color(0));//beying lazy/having fun with text: the \u25A0 is using the unicode for a square shape as text http://www.fileformat.info/info/unicode/char/25a0/index.htm
Button ellipseMode = new Button("\u25CF",5,30,30,20,color(200),color(0));//http://www.fileformat.info/info/unicode/char/25CF/index.htm
Button color1 = new Button("",5,55,30,20,color(255,0,0),c1);
Button color2 = new Button("",5,80,30,20,color(255,255,0),c2);
Button color3 = new Button("",5,105,30,20,color(00,0,255),c3);
Button[] gui = new Button[] {rectMode, ellipseMode, color1, color2, color3};
//reference to previous mode button and previous colour button
Button prevMode = rectMode;
Button prevColor = color1;
void setup(){
size(400,400);
//setup ellipse mode to draw from corner like the rect()'s default setting
ellipseMode(CORNER);
strokeWeight(3);
//initialise the canvas - this allows you to draw with the same commands, but as a separate layer
canvas = createGraphics(width,height);
canvas.beginDraw();
//replicate ellipse mode and stroke weight in canvas layer as well (so what's being drawn matches preview)
canvas.ellipseMode(CORNER);
canvas.strokeWeight(3);
canvas.background(255);
canvas.endDraw();
//ui outline current options
rectMode.outline = true;
color1.outline = true;
}
void draw(){
//draw the layer first
image(canvas,0,0);
//overlay the preview on top using 50% transparency (as a visual hint it's a preview)
draw(g,127);
//update and draw UI
for(int i = 0; i < gui.length; i++){
gui[i].update(mouseX,mouseY,mousePressed);
gui[i].draw();
}
}
//a function that draws into a PGraphics layer (be it our canvas or Processing's)
void draw(PGraphics g,int transparency){
g.fill(current,transparency);
//compute width,height as difference between current and previous mouse positions
float w = mouse.x - pmouse.x;
float h = mouse.y - pmouse.y;
//draw the shape according to it's mode
if(mode == MODE_ELLIPSE) {
g.ellipse(pmouse.x,pmouse.y,w,h);
}
if(mode == MODE_RECTANGLE) {
g.rect(pmouse.x,pmouse.y,w,h);
}
}
//set both previous and current mouse coordinates - this helps reset coordinates and finalize a shape
void mouseSet(){
pmouse.set(mouseX,mouseY);
mouse.set(mouseX,mouseY);
}
void mousePressed(){
mouseSet();
}
void mouseDragged(){//update only the current mouse position, leaving pmouse outdated
mouse.set(mouseX,mouseY);
}
void mouseReleased(){
//commit the shape to the canvas layer
canvas.beginDraw();
draw(canvas,255);
canvas.endDraw();
//set both mouse positions
mouseSet();
}
void onButtonClicked(Button b){
if(b == color1) current = c1;
if(b == color2) current = c2;
if(b == color3) current = c3;
if(b == rectMode) mode = MODE_RECTANGLE;
if(b == ellipseMode) mode = MODE_ELLIPSE;
if(b == color1 || b == color2 || b == color3){
b.outline = true;
if(prevColor != null) prevColor.outline = false;
prevColor = b;
}
if(b == rectMode || b == ellipseMode){
b.outline = true;
if(prevMode != null) prevMode.outline = false;
prevMode = b;
}
}
class Button{
float w,h,x,y;//width, height and position
color bg = color(200);//background colour
color fg = color(0);//foreground colour
String label;//text displayed
boolean isOver,wasPressed;//button states
int pw = 10;//padding on width
boolean outline;//draw an outline or not
Button(String label,float x,float y,float w,float h,color fg,color bg){
this.x = x;
this.y = y;
this.w = w;
this.h = h;
this.label = label;
this.fg = fg;
this.bg = bg;
}
void update(int mx,int my,boolean md){
//check bounding box
isOver = ((mx >= x && mx <= (x+w))&&(my >= y && my <= (y+h)));
if(isOver && md){
//check if it was not previously pressed to call the onButtonClicked function only once (similar to debouncing)
if(!wasPressed){
onButtonClicked(this);
wasPressed = true;
}
}else wasPressed = false;
}
void draw(){
//pushStyle()/popStyle() isolates drawing styles (similar to how pushMatrix()/popMatrix() isolates coordinate transformations
pushStyle();
if(outline){
strokeWeight(3);
stroke(127);
}else{
noStroke();
}
fill(isOver ? fg : bg);//the ? : is a lazy one liner way of doing if/else: (booleanExpression ? doIfTrue : doIfFalse)
rect(x,y,w,h);
fill(isOver ? bg : fg);
text(label,x+pw,y+h*.75);
popStyle();
}
}
请注意,一些代码正在处理先前选择的颜色和形状模式按钮。这实际上不是必需的,但是向用户显示一些关于当前选择的形状/颜色的反馈(在本例中以轮廓的形式)是很好的。
在 UI 方面还有很多需要探索的地方。例如,尽管在功能方面有多个按钮,但它们主要用作两个单选按钮组。一旦掌握了 OOP 基础知识,您就可以考虑创建一个通用的 GUIElement 类,例如按钮/复选框/单选按钮/滑块等其他 UI 元素可以使用(例如 draw() 函数、x、y、宽度、高度, ETC。)。然后每个班级都会专门研究这个超级班级。例如 Button 将扩展 GUIElement 并且 ToggleButton 将扩展 Button。也许 HBox 或 VBox 可以方便地将元素分组到水平或垂直组中。等等。玩得开心!