Проблема с перерисовкой/обновлением Java Jpanel

у меня проблема с методом Repaint моей jpanel. Я пытаюсь сделать "Гоночную игру" с Java Swing и следуя архитектуре MVC:

у меня есть 5 классов: Main: запускает MVC

public class Main {
    private static Model model;
    private static View view;
    private static Controller controller;

    public static void main(String[] args) {
        model = new Model();
        view =  new View();
        controller = new Controller();

        model.addObserver(view);

        controller.addModule(model);
        controller.addView(view);

        view.addContoller(controller);
    }
}

Модель:

import java.util.ArrayList;
import java.util.Observable;


public class Model extends Observable{
    private ArrayList<Car> cars;// List of cars

    public Model() {
        cars = new ArrayList<Car>();
        cars.add(new Car(this,0, 50));
        cars.add(new Car(this,0, 50));
        cars.add(new Car(this,0, 50));
    }

    public void startCar(int i){
        //i is the index of the selected element in the checkbox
        //if i==0 then the selected element is "All cars" else is a specific car
        if(i>0)
            cars.get(i-1).start();
        else{
            for(int j=0;j<cars.size();j++)
                cars.get(j).start();
        }
    }

    public void speedUpCar(int i) {
        if(i>0)
            cars.get(i-1).incVitess();
        else{
            for(int j=0;j<cars.size();j++)
                cars.get(j).incVitess();
        }
    }

    public void notifyView(){
        setChanged();
        notifyObservers(cars);
    }

    public void speedDownCar(int i) {
        if(i>0)
            cars.get(i-1).decVitess();
        else{
            for(int j=0;j<cars.size();j++)
                cars.get(j).decVitess();
        }
    }

    public void stopCar(int i) {
        if(i>0)
            cars.get(i-1).stop();
        else{
            for(int j=0;j<cars.size();j++)
                cars.get(j).stop();
        }
    }
}

вид :

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.GridLayout;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
import java.util.Observable;
import java.util.Observer;
import java.util.Vector;

import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JPanel;


public class View implements Observer {
    private JFrame fen;
    private JPanel btnPanel,panel;
    private JButton play,speedUp,speedDown,stop;
    private JComboBox<String> listeCar;
    private boolean test = false;

    public View() {
        fen = new JFrame();
        fen.setTitle("Car Racing");
        fen.setSize(900, 400);
        fen.setLocationRelativeTo(null);
        fen.setResizable(false);

        Vector<String> v = new Vector<String>();
        v.add("All cars");v.add("car 1");v.add("car 2");v.add("car 3");
        listeCar = new JComboBox<String>(v);

        play = new JButton("Play");
        speedUp = new JButton("+");
        speedDown = new JButton("-");
        stop = new JButton("stop");

        panel = new JPanel(new GridLayout(3,1));
        btnPanel = new JPanel();

        btnPanel.add(listeCar);
        btnPanel.add(play);
        btnPanel.add(speedUp);
        btnPanel.add(speedDown);
        btnPanel.add(stop);

        Container c = fen.getContentPane();

        c.setLayout(new BorderLayout());

        c.add(btnPanel, BorderLayout.SOUTH);
        c.add(panel, BorderLayout.CENTER);

        fen.setVisible(true);

        fen.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);
            }
        });


    }

    public void addContoller(Controller controller){
        play.addActionListener(controller);
        speedUp.addActionListener(controller);
        speedDown.addActionListener(controller);
        stop.addActionListener(controller);
    }

    @Override
    public void update(Observable arg0, Object c) {
        ArrayList<Car> cars = (ArrayList<Car>)c;
        for(int i=0;i<cars.size();i++){
            Car car = cars.get(i);
            if(!test){ // if its the first tima, add the cars to the panel
                panel.add(car);
            }else{
                car.repaint(); // << the Problem is HERE
            }
        }
        test = true;
    }

    public JButton getPlay() {
        return play;
    }

    public JButton getSpeedUp() {
        return speedUp;
    }

    public JButton getSpeedDown() {
        return speedDown;
    }

    public JButton getStop() {
        return stop;
    }

    public JComboBox<String> getListeCar() {
        return listeCar;
    }
}

Контроллер:

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;


public class Controller implements ActionListener{
    private Model model;
    private View view;

    public Controller() {

    }

    public void addModule(Model m) {
        model = m;
        model.notifyView();
    }

    public void addView(View v){
        view = v;
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        if(e.getSource() == view.getPlay()){
            model.startCar(view.getListeCar().getSelectedIndex());
        }else if(e.getSource() == view.getSpeedUp()){
            model.speedUpCar(view.getListeCar().getSelectedIndex());
        }else if(e.getSource() == view.getSpeedDown()){
            model.speedDownCar(view.getListeCar().getSelectedIndex());
        }else if(e.getSource() == view.getStop()){
            model.stopCar(view.getListeCar().getSelectedIndex());
        }
    }
}

Класс автомобиля:

import java.awt.Color;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;
import javax.swing.JPanel;


public class Car extends JPanel{
    private int id,x,y,vitess;
    private Thread thread;
    private Model model;
    private boolean start = true;
    private boolean forward = true;
    private Color color;
    private boolean threadStarted = false;
    private BufferedImage bg; // background image

    public Car(Model model,int x,int y) {
        this.x =x;
        this.y = y;
        vitess = 7;
        this.model = model;

        try {
            bg = ImageIO.read(new File("road.png"));
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        color = changeColor(); // Random color

        thread = new Thread(new CarThread(this));

        start();
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        g.drawImage(bg, 0, 0, null); 
        g.setColor(color);
        g.fillRect(x, y, 100, 50); // the car is a simple rectangle
    }

    public void start() {
        start = true;
        if(!threadStarted){
            threadStarted = true;
            thread.start();
        }else{
            thread.resume();
        }
    }

    public void move(){
        System.out.println("X:"+x);
        if(forward){
            if(x<this.getWidth()){
                x+=2;
            }else{
                color = changeColor();
                forward = false;
            }
        }else{
            if(x>0){
                x-=2;
            }else{
                color = changeColor();
                forward = true;
            }
        }

        model.notifyView();
    }

    private Color changeColor() {
        int r = (int)(Math.random()*255);
        int g = (int)(Math.random()*255);
        int b = (int)(Math.random()*255);
        return new Color(r,g,b);
    }

    public void stop(){
        start = false;
        thread.suspend();
    }

    public void incVitess(){
        if(vitess>1)
            vitess--;
    }

    public void decVitess(){
        if(vitess<6)
            vitess++;
    }

    public int getId() {
        return id;
    }


    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public int getVitess() {
        return vitess;
    }   

    public boolean getStart(){
        return start;
    }

    public void setStart(boolean m){
        this.start = m;
    }

    public Color getColor(){
        return color;
    }
}

Класс CarThraed:

public class CarThread implements Runnable{
    private Car car;

    public CarThread(Car car) {
        this.car = car;
    }

    @Override
    public void run() {
        while(car.getStart()){
            car.move();
            try {
                Thread.sleep(car.getVitess());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}

если вы запустите проект, вы заметите, что автомобили не доходят до конца кадра, даже если они используются:

if(x<this.getWidth()) x++;

но когда я заменю

car.repaint(); // << the Problem is HERE

с участием

car.update(car.getGraphics()); // << the Problem is HERE

теперь машины могут доходить до конца фрейма, но кнопки в btnJpanel исчезают

изображение с перерисовкой здесь изображение с обновлением здесь

заранее спасибо


person EssaidiM    schedule 23.11.2014    source источник


Ответы (1)


Не могу сказать, что прочитал весь ваш код, но ваша модель никогда не уведомляет представление. Я имею в виду, что нигде в коде класса модели не вызывается метод notifyView(). Модель должна нести ответственность за вызов этого всякий раз, когда ее состояние изменяется.

Обратите внимание, что это:

car.update(car.getGraphics()); // << the Problem is HERE

Не следует использовать, так как полученная графика нестабильна.

Кроме того, ваша модель содержит компоненты представления, ArrayList of Car, класс, который расширяет JPanel, еще одна вещь, которая никогда не должна происходить, поскольку модель должна полностью игнорировать представление, за исключением того, что она знает, что некоторые вещи могут прослушиваться. это, и ему нужно уведомить об этих вещах, вот и все. Вместо этого ваша Модель должна содержать ArrayList логических объектов Car без представления, не принадлежащих JPanel.


Изменить
Вы заявляете:

В модели у меня есть метод: public void notifyView(), который вызывается методом автомобиля public void Move(). Это означает, что всякий раз, когда поток вызывает метод перемещения автомобиля, он вызывает notifyView модели.

Нет, Car не должен вызывать этот метод — его должна вызывать только сама модель при изменении ее состояния.

Кроме того, я вижу, что у вас очень опасный код, использующий вызовы методов Thread#suspend() и Thread#resume(). Эти методы устарели, поскольку были признаны опасными. Чтобы узнать почему, ознакомьтесь с API для потоков, а также эту полезную статью. Вы определенно захотите избежать их использования.


Предложения

  • Сделайте Car логическим классом без графического интерфейса, который знает свою позицию и может изменить свою позицию.
  • Переопределите свой метод рисования JPanels protected void paintComonent(Graphics g) и используйте информацию об автомобиле из модели для рисования автомобилей. Другими словами, используйте состояние модели, чтобы влиять на состояние представления.
  • Используйте Swing Timer вместо фонового потока, чтобы изменить положение вашего автомобиля.
  • Пусть модель и только модель вызывают метод notifyView при изменении ее состояния.

Изменить
Основная ошибка здесь:

class Car extends JPanel {
    private int id, x, y, vitess;

    //....

    public int getX() {
       return x;
    }

    public int getY() {
       return y;
    }

Вы непреднамеренно переопределяете методы getX и getY Car JPanel, искажая расположение этих компонентов. Это еще одна причина избегать переопределения компонентов Swing без необходимости - чтобы избежать этих скрытых побочных эффектов.

Избавьтесь от этих методов или переименуйте их.

person Hovercraft Full Of Eels    schedule 23.11.2014
comment
В модели у меня есть метод: public void notifyView(), который вызывается методом автомобиля public void Move(). Это означает, что всякий раз, когда поток вызывает метод перемещения автомобиля, он вызывает notifyView модели. - person EssaidiM; 23.11.2014
comment
@EssaidiM Ну, есть проблема, вы изменяете представление, которое обновляет режим, который уведомляет представление ?? Поток должен уведомлять модель, которая обновляет представление. Представление будет само обновляться, чтобы представлять состояние модели... и Swing не является потокобезопасным... - person MadProgrammer; 23.11.2014
comment
@EssaidiM Кроме того, ваш контроллер должен быть связующим звеном между вашим представлением и моделью, представление и модель не должны взаимодействовать напрямую (если вы хотите чистый MVC) - person MadProgrammer; 23.11.2014
comment
@EssaidiM: хотя поймите, что есть варианты MVC, в которых модель уведомляет представление, и это законно, если ей следуют, но ваш этого не делает. - person Hovercraft Full Of Eels; 23.11.2014
comment
Хорошо, я попытаюсь исправить свой MVC и проверить, делает ли метод перерисовки то, что я ожидаю. и я могу заменить приостановку и возобновление с чем? - person EssaidiM; 23.11.2014
comment
@EssaidiM: используйте Swing Timer, который имеет простые в использовании методы stop() и start(). - person Hovercraft Full Of Eels; 23.11.2014