processing - Having Object Oriented programming is

2020-03-28 09:50发布

问题:

So basically, my aim it to create a program like the MS Paint software, where I can draw shapes with mouse dragging and change its colors. This is a very long script of mine. I'm fairly new to OOP so I am having difficulties trying to make all the functions to work together.

Main

Button button1, button2, button3, button4, button5, button6, button7;
Rect rect;
Circle circle;
int mode = 1;

void setup() {
    size(900,600);
    smooth();
    rect = new Rect(0,0,0,0, new PImage());
    circle = new Circle(0,0,0,0, new PImage());
    color gray = color(234);
    color black = color(0);
    color white  = color(255);
    color red = color(255,0,0);
    color green = color(0,255,0);
    color blue = color(0,0,255);

    button1 = new Button(10, 60, 20, white, gray, black); //draw rectangle function
    button2 = new Button(10, 100, 20, white, gray, black); //draw circle function
    button3 = new Button(10, 140, 20, red, gray, black); //option of color red
    button4 = new Button(10, 160, 20, green, gray, black); //option of color green
    button5 = new Button(10, 180, 20, blue, gray, black); //option of color blue
    button6 = new Button(10, 220, 20, black, gray, black); //fill entire shape
    button7 = new Button(10, 240, 20, white, gray, black); //fill nothing
}

void draw() {
    button1.setp();
    button2.setp();
}

void mousePressed() {
    if (button1.press()) { mode = 1; }
    if (button2.press()) { mode = 2; }
    if (button3.press()) { mode = 3; }
    if (button4.press()) { mode = 4; }
    if (button5.press()) { mode = 5; }
    if (button6.press()) { mode = 6; }
    if (button7.press()) { mode = 7; }
}

void manageButtons() {
    button1.update();
    button2.update();
    button3.update();
    button4.update();
    button5.update();
    button6.update();
    button7.update();

    button1.display();
    button2.display();
    button3.display();
    button4.display();
    button5.display();
    button6.display();
    button7.display();
}

void mouseReleased() {
    button1.release();
    button2.release();
    button3.release();
    button4.release();
    button5.release();
    button6.release();
    button7.release();
}

void mouseDragged() {
    //rect.drag();
}

Button Class

class Button {
    int x, y; // the x- and y-coordinate
    int size; // dimension (width & height)
    color baseGray; // Default gray value
    color overGray; // Value when the mouse is over
    color pressGray; // Value when the mouse is pressed
    boolean over = false; // true when the mouse is over
    boolean pressed = false;// true when pressed 

    Button(int xp, int yp, int s, color b, color o, color p) {
        x = xp;
        y = yp;
        size = s;
        baseGray = b;
        overGray = o;
        pressGray = p;
    }

    void setp() {
        background(255);
        manageButtons();
        //stroke();
        if (mode == 1) {
            rect.drawing();
        } else if (mode == 2) {
            circle.drawing();
        }
    }

    void update() {
        if ((mouseX >= x) && (mouseX <= x + size) && (mouseY >= y) && (mouseY <= y + size)) {
            over = true;
        } else {
            over = false;
        }
    }

    boolean press() {
        if (over) {
            pressed = true;
            return true;
        } else {
            return false;
        }
    }

    void release() {
        pressed = false;
        rect.release();
        circle.release();
    }

    void display() {
        if (pressed) {
            fill(pressGray);
        } else if (over) {
            fill(overGray);
        } else {
            fill(baseGray);
        }

        stroke(0);
        rect(x, y, size, size);
    }
}

Circle Class

class Circle {
    int x, y;
    int xp, yp;
    PImage a;

    Circle(int dragx, int dragy, int movex, int movey, PImage image) {
        x = dragx;
        y = dragy;
        xp = movex;
        yp = movey;
        a = image;
    }

    void display() {
        smooth();
        background(255);
        a = get();
        stroke(0);
        fill(255); //255,255,10);
    }

    void drawing() {
        image(a, 0, 0); //background(a);
        float sizex = xp - x;
        float sizey = yp - y;
        if (mousePressed && mouseButton == LEFT) {
            ellipse(x, y, sizex, sizey);
        }
    }

    void press() {
        x = mouseX;
        y = mouseY;
    }

    void release() {
        xp = mouseX;
        yp = mouseY;
        noLoop();
        a = get();
        loop();
    }

    void drag() {
        xp = 80 + mouseX;
        yp = 80 + mouseY;
    }
}

Rectangle Class

class Rect {
    int x, y;
    int xp, yp;
    PImage a;

    Rect(int dragx, int dragy, int movex, int movey, PImage image) {
        x = dragx;
        y = dragy;
        xp = movex;
        yp = movey;
        a = image;
    }

    void display() {
        smooth();
        background(255);
        a = get();
        stroke(0);
        fill(255); //255,255,10);
    }

    void drawing() {
        image(a, 0, 0); //background(a);
        float sizex = xp - x;
        float sizey = yp - y;
        if (mousePressed && mouseButton == LEFT) {
            rect(x, y, sizex, sizey);
        }
    }

    void press() {
        x = mouseX;
        y = mouseY;
    }

    void release() {
        xp = mouseX;
        yp = mouseY;
        noLoop();
        a = get();
        loop();
    }

    void drag() {
        xp = mouseX;
        yp = mouseY;
    }
}

With the above processing script (java), I am having issues getting the rectangle and circle classes to work properly with the buttons I have created. Button 1 is supposed to draw rectangles and button 2 is supposed to draw circles (so far, only these functions are supposed to work. I also need to apply colors to them).

I know the rectangle and circle classes work properly because I had tested them separately prior to putting everything together. The buttons (functions) work but not correctly (I am supposed to use the mouse to drag the shapes into any desired place, while this program only allows them to appear where I place them and only from the corner, as if I'm only stretching the shapes from x and y position (0,0)). My sole problem is that I can't seem to join the buttons to the shape function correctly, and make them all work together.

回答1:

It is hard to evaluate the correctness of your code without seeing the main method. Your 'main' class is set up in such a way that it appears the method mousePressed() is intended to poll the buttons and set the drawing mode based on their state.

It is impossible for me to know if you are polling the buttons correctly, however, without viewing the main method.

If you are looking to use an Object-Oriented approach, you are going to want to use the Observer pattern. In essence, your buttons will all have a reference to the 'main' object which has a buttonClicked(Button btn) method. When a button is clicked, it runs the 'main' object's buttonClicked(Button btn) method. The argument provided will be a reference to the button that is clicked so that the main can choose the appropriate mode to use. Demo code follows:

In Main class:

//Give the button a reference to the main object
button1 = new button(this);

//receive notifications from the button
public buttonClicked(Button btn) {
    if(btn.equals(button1))
        mode = 1;
    if(...)
        ...
}


回答2:

Having a quick at your code it looks like there is some confusion over using drawing commands, writing classes and how these interact. I would start really really basic:

  • split everything into simple easy to digest standalone tasks
  • implement each task independently and test it
  • simplify and generalise the code if needed
  • start bringing implementations together into a main project, one by one, testing at each step.

For example, let's take the task of interactively drawing a rectangle using the mouse. If you store the position of the mouse when it was pressed, you can take the difference between them and the coordinates to the most recent mouse coordinates: the dimensions of the rectangle:

//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();
}

If you want to render a preview of the shape, and the shape itself, using layers(like in Photoshop) might be handy: one layers renders what's been drawn and another temporarily renders a preview on top.

Luckily there is something like in Processing via PGraphics. Once you initialize a PGraphics instance, you can draw into it using the same drawing commands you're used to. The only catch is that you need to call beginDraw() first, then endDraw() at the end.

The other cool thing about PGraphics is that it extends PImage which means you can display it as one (not to mention, do image maniplations):

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);

In your code the button class keeps references to rectangles and other objects. Ideally you want objects to be loosely coupled. The idea is to keep classes dealing with their own functionality, independent of the main program or other classes. Try taking copying your Button class into a new sketch and using straight away. At the moment you need to copy the other unrelated classes as well just to compile. The goal would be to write a Button class that can easily be reused in any sketch.

Going back to the main functionality, you would need:

  1. a drawing shape mode (rectangle or ellipse)
  2. a drawing colour (from a list of colours)

This implies there are multiple shapes and multiple colours, but one of each used at a time. Putting the above ingredients together, you can prototype the functionality without a GUI or classes initially if it's easier. Simply use keyboard shortcuts to replace GUI buttons for this testing phase (use 1/2/3 to control colours, r for rectangle and c for circle):

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; 
}

Remember how we used PGraphics before ? Each Processing applet has one already, named g, so that's a quick and dirty way of using a single function to draw into multiple PGraphics instance (Processing's and our canvas) but with different transparencies for each.

With this approach it's worth noting that rather than storing multiple Circle/Rectangle instances for each shape drawn, we simple render once into the canvas and have it (one PGraphics) object store the pixels. The downside is once it's drawn, you can't retrieve what shape was drawn in which order and with what coordinates/dimensions, but if you don't need these details it's simpler.

In case you do need these, might be worth checking out PShape (and especially it's GROUP,RECT,ELLIPSE options).

Now let's add buttons! Anthony's suggestion is great and the simplified buttonClicked suggestion lends itself well to Processing. Normally you'd use an Interface to define a listener for the button and have the Processing sketch implement this interface, but simply having a single function responsible for handling buttons in a a nice workaround.

Here's a basic implementation:

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();
  }
}

Now it's fairly straight forward to add this Button class to the previous code using keyboard shortcuts only:

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();
  }
}

Note that some of code is handling the previously selected colour and shape mode buttons. This isn't actually needed, but it's nice to show the user some feedback to what shape/colour is currently selected (in the form of an outline in this case).

In terms of UI there is a lot more to explore. For example, although there are multiple buttons in terms of the functionality they allow there mainly work as two radio button groups. Once you get the hang of OOP Basics you can look into creating a common GUIElement class for example, which other UI elements like buttons/checkboxes/radio buttons/sliders can use (e.g. a draw() function, x,y, width, height, etc.). Then each class would specialise this super class. For example Button will extend GUIElement and ToggleButton will extend Button. Perhaps an HBox or VBox would be handy to easily group elements in a horizontal or vertical group. etc. Have fun!