为什么仅在某些 JPanel 上调用 paintComponent?

Why does paintComponent get called only on some JPanels?

我正在尝试为不同的 JPanel 添加背景(以后它们将被称为 Window)。这些 Windows 是我创建的 classes 并使它们继承 JPanel。然后根据程序的状态,将 Window 设置为程序的 JFrame 的内容面板。当在某些 Windows 中设置了背景而在其他人中没有设置时,问题就来了。背景设置是使用paintComponent(Graphics g)方法进行的,但是尽管我尝试修复了这个错误,但我没有成功。

这是我认为可能对那些想要帮助的人有用的代码:

主循环:

public class Game{

//here comes other stuff (constructor, main, other methods...)

    private void run(){
        while(true){
            if(GameState.changed){
                Screen.getInstance().seeWindow(state);
                GameState.changed = false;
            }else {
                Screen.getInstance().requestFocus(state);
            }
        }
    }
}

屏幕class:

package view;

import game.GameState;
import view.wins.*;

import javax.swing.*;
import java.awt.*;

public class Screen extends JFrame {
    private final int WIDTH;
    private final int HEIGHT;

    private static Screen instance = null;
    private JComponent titleWindow, menuWindow, settingsWindow;

    private Screen(){
        WIDTH = 1152;
        HEIGHT = 768;

        setTitle("Game");
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
        this.setLocation(dim.width/2-WIDTH/2, (dim.height - 50)/2-HEIGHT/2);
        Dimension size = new Dimension(WIDTH, HEIGHT);

        setPreferredSize(size);
        setMinimumSize(size);
        setMaximumSize(size);
        setSize(size);

        setResizable(false);
        setVisible(true);
    }

    public static Screen getInstance() {
        if(instance == null){
            instance = new Screen();
        }
        return instance;
    }

    public void seeWindow(GameState state){
        switch(state){
            case TITLE -> setContentPane(getTitleWindow());
            case MENU -> setContentPane(getMenuWindow());
            case SETTINGS -> setContentPane(getSettingsWindow());
        }
        pack();
    }

    public void requestFocus(GameState state){
        switch (state){
            case TITLE -> getTitleWindow().requestFocus();
            case MENU -> getMenuWindow().requestFocus();
            case SETTINGS -> getSettingsWindow().requestFocus();
        }
    }

    private JComponent getTitleWindow(){
        if(titleWindow == null){
            titleWindow = new TitleWindow();
        }
        return titleWindow;
    }

    private JComponent getMenuWindow(){
        if(menuWindow == null){
            menuWindow = new MenuWindow();
        }
        return menuWindow;
    }

    private JComponent getSettingsWindow(){
        if(settingsWindow == null){
            settingsWindow = new SettingsWindow();
        }
        return settingsWindow;
    }
}

Window摘要class:

package view.wins;

import utilz.GFXManager;
import view.Screen;

import javax.swing.*;
import java.awt.*;
import java.util.Observer;

public abstract class Window extends JComponent implements Observer {
    private Image background;

    public Window(String background){
        setLayout(new BorderLayout());
        setPreferredSize(Screen.getInstance().getPreferredSize());

        setBackground(background);
        setFocusable(true);
    }

    protected void setBackground(String backgroundName){
        this.background = GFXManager.getInstance().getImage("backgrounds/" + backgroundName + ".png");
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        g.drawImage(background, 0, 0, null);
    }

}

A Window 背景设置正确:

package view.wins;

import game.GameState;
import game.Game;
import jdk.swing.interop.SwingInterOpUtils;
import logic.TitleLogic;
import utilz.GFXManager;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.Observable;
import java.util.Observer;

public class TitleWindow extends Window implements Observer {
    private final String BELOW_TITLE_TEXT;
    private final ImageIcon TITLE_ICON;

    private JLabel lblTitleIcon, lblBelowTitle;
    private KeyController keyController;

    public TitleWindow(){
        super("title_background");
        BELOW_TITLE_TEXT = "Press enter to start";
        TITLE_ICON = new ImageIcon(GFXManager.getInstance().getImage("texts/title.png"));

        TitleLogic.getInstance().addObserver(this);

        setLayout(new BorderLayout());
        addKeyListener(new KeyController());

        add(getLblTitleIcon(), BorderLayout.CENTER);
        add(getLblBelowTitle(), BorderLayout.SOUTH);
    }

    private JLabel getLblTitleIcon(){
        if(lblTitleIcon == null){
            lblTitleIcon = new JLabel(TITLE_ICON);
        }
        return lblTitleIcon;
    }

    private JLabel getLblBelowTitle(){
        if(lblBelowTitle == null){
            lblBelowTitle = new JLabel(BELOW_TITLE_TEXT, SwingConstants.CENTER);
            lblBelowTitle.setFont(new Font("MS Gothic", Font.PLAIN, 24));
            lblBelowTitle.setForeground(new Color(30,230,120));
        }
        return lblBelowTitle;
    }

    private KeyController getKeyController(){
        if(keyController == null){
            keyController = new KeyController();
        }
        return keyController;
    }

    @Override
    public void update(Observable o, Object arg) {
        if(TitleLogic.getInstance().isTickColorChange()){
            getLblBelowTitle().setForeground(new Color(120, 30, 230));
        }else{
            getLblBelowTitle().setForeground(new Color(30,230,120));
        }
    }

    private class KeyController extends KeyAdapter {
        @Override
        public void keyTyped(KeyEvent e) {
            if(e.getKeyChar() == '\n'){
                Game.getInstance().setState(GameState.MENU);
            }else if(e.getKeyChar() == 'c'){
                TitleLogic.getInstance().tickColorChange();
            }
        }
    }
}

未设置背景的Window:

package view.wins;

import game.GameState;
import game.Game;
import logic.MenuLogic;
import view.objs.Button;

import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Observable;
import java.util.Observer;

public class MenuWindow extends Window implements Observer {
    private JPanel btnPanel;
    private Button btnStartNewGame, btnLoadGame, btnSettings, btnExit;
    private Controller controller;

    public MenuWindow(){
        super("title_background");
        MenuLogic.getInstance().addObserver(this);

        setLayout(new BorderLayout());

        add(getBtnPanel(), BorderLayout.CENTER);
    }

    private JPanel getBtnPanel(){
        if(btnPanel == null){
            btnPanel = new JPanel(new GridLayout(4,1));

            btnPanel.add(getBtnStartNewGame());
            btnPanel.add(getBtnLoadGame());
            btnPanel.add(getBtnSettings());
            btnPanel.add(getBtnExit());
        }
        return btnPanel;
    }

    private Button getBtnStartNewGame(){
        if(btnStartNewGame == null){
            btnStartNewGame = new Button("mediumLong", "Start new game", getController());
        }
        return btnStartNewGame;
    }

    private Button getBtnLoadGame(){
        if(btnLoadGame == null){
            btnLoadGame = new Button("mediumLong", "Load game", getController());
        }
        return btnLoadGame;
    }

    private Button getBtnSettings(){
        if(btnSettings == null){
            btnSettings = new Button("mediumLong", "Settings", getController());
        }
        return btnSettings;
    }

    private Button getBtnExit(){
        if(btnExit == null){
            btnExit = new Button("mediumLong", "Exit", getController());
        }
        return btnExit;
    }

    private Controller getController(){
        if(controller == null){
            controller = new Controller();
        }
        return controller;
    }

    @Override
    public void update(Observable o, Object arg) {

    }

    private class Controller extends MouseAdapter {
        @Override
        public void mouseClicked(MouseEvent e) {
            if(e.getSource().equals(getBtnStartNewGame())){

            }else if(e.getSource().equals(getBtnLoadGame())){

            }else if(e.getSource().equals(getBtnSettings())){
                Game.getInstance().setState(GameState.SETTINGS);
            }else if(e.getSource().equals(getBtnExit())){
                System.exit(0);
            }
        }

        @Override
        public void mouseEntered(MouseEvent e) {
            ((Button)e.getSource()).changeHighlight();
        }

        @Override
        public void mouseExited(MouseEvent e) {
            ((Button)e.getSource()).changeHighlight();
        }
    }
}

按钮是我自己的class。

如果有人想自己测试它或查看更多代码 here 是 github 存储库。

我尝试了一切,甚至更多。我注意到 MenuWindow 的 Graphics 没有被初始化,所以问题可能是因为 window 没有被渲染。我不知道。

这个...

private void run(){
    while(true){
        if(GameState.changed){
            Screen.getInstance().seeWindow(state);
            GameState.changed = false;
        }else {
            Screen.getInstance().requestFocus(state);
        }
    }
}

是个坏主意。除了“野循环”本身通常不是一个好主意这一事实之外,Swing 也不是线程安全的并且是单线程的。

这意味着如果这是 运行 在事件调度线程的上下文中,它将阻止它并阻止它处理任何新事件。如果它不在 EDT 中 运行,您就有可能导致任何数量的图形故障或其他“脏线程”问题。

再加上像这样的“狂野循环”也会消耗 CPU 个周期这一事实,您会增加巨大的性能开销,但收效甚微。

先看看 Swing 中的并发

Screen.getInstance().requestFocus(state); 也是对 KeyListener 已知局限性的破解,通过使用 key bindings API

可以更好地解决这些局限性

考虑到所有这些因素,Kavaliro 应该看起来更像...

package game;

import view.Screen;

public class Kavaliro {
    private static Kavaliro instance = null;

    private GameState state;

    private Kavaliro() {
        state = GameState.TITLE;
        Screen.getInstance().seeWindow(state);
    }

    public static Kavaliro getInstance() {
        if (instance == null) {
            instance = new Kavaliro();
        }
        return instance;
    }

    public static void main(String[] args) {
        Kavaliro game = Kavaliro.getInstance();
    }

    public void setState(GameState state) {
        Screen.getInstance().seeWindow(state);
    }
}

这也不是在 Java 中创建单例的好方法。

public static Kavaliro getInstance() {
    if (instance == null) {
        instance = new Kavaliro();
    }
    return instance;
}

你可以看看Java Singleton Design Pattern Best Practices with Examples and What is an efficient way to implement a singleton pattern in Java? OR you could just make use of Dependency Injection (and probably some research into dependency injection vs singleton)


现在,正如我所说,KeyListener 通常是监视用户键盘输入的一个糟糕选择,这意味着 TitleWindow 应该改变,更像是...

package view.wins;

import game.GameState;
import game.Kavaliro;
import game.Utilities;
import logic.TitleLogic;
import utilz.GFXManager;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.Observable;
import java.util.Observer;

public class TitleWindow extends Window implements Observer {
    private final String BELOW_TITLE_TEXT;
    private final ImageIcon TITLE_ICON;

    private JLabel lblTitleIcon, lblBelowTitle;

    public TitleWindow() {
        super("title_background");
        BELOW_TITLE_TEXT = "Press enter to start";
        TITLE_ICON = new ImageIcon(GFXManager.getInstance().getImage("texts/title.png"));

        TitleLogic.getInstance().addObserver(this);

        setLayout(new BorderLayout());

        Utilities.addKeyBinding(this, Utilities.keyStrokeFor(Utilities.Input.ENTER), new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                Kavaliro.getInstance().setState(GameState.MENU);
            }
        });
        Utilities.addKeyBinding(this, Utilities.keyStrokeFor(Utilities.Input.TITLE_TOGGLE), new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                TitleLogic.getInstance().tickColorChange();
            }
        });

        add(getLblTitleIcon(), BorderLayout.CENTER);
        add(getLblBelowTitle(), BorderLayout.SOUTH);
    }

    private JLabel getLblTitleIcon() {
        if (lblTitleIcon == null) {
            lblTitleIcon = new JLabel(TITLE_ICON);
        }
        return lblTitleIcon;
    }

    private JLabel getLblBelowTitle() {
        if (lblBelowTitle == null) {
            lblBelowTitle = new JLabel(BELOW_TITLE_TEXT, SwingConstants.CENTER);
            lblBelowTitle.setFont(new Font("MS Gothic", Font.PLAIN, 24));
            lblBelowTitle.setForeground(new Color(30, 230, 120));
        }
        return lblBelowTitle;
    }

    @Override
    public void update(Observable o, Object arg) {
        if (TitleLogic.getInstance().isTickColorChange()) {
            getLblBelowTitle().setForeground(new Color(120, 30, 230));
        } else {
            getLblBelowTitle().setForeground(new Color(30, 230, 120));
        }
    }
}

现在,为了让生活更轻松,我写了一个快速的“实用程序”class...

package game;

import java.awt.event.KeyEvent;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JComponent;
import static javax.swing.JComponent.WHEN_IN_FOCUSED_WINDOW;
import javax.swing.KeyStroke;

public class Utilities {

    public static enum KeyState {
        PRESSED, RELEASED
    }

    public static enum Input {
        ENTER(KeyEvent.VK_ENTER), TITLE_TOGGLE(KeyEvent.VK_C);

        private int keyEvent;

        private Input(int keyEvent) {
            this.keyEvent = keyEvent;
        }

        protected KeyStroke getKeyStroke(int modifiers, KeyState keyState) {
            return KeyStroke.getKeyStroke(keyEvent, 0, keyState == KeyState.RELEASED ? true : false);
        }
    }

    public static void addKeyBinding(JComponent parent, KeyStroke keyStroke, Action action) {
        addKeyBinding(parent, keyStroke.toString(), keyStroke, action);
    }

    public static void addKeyBinding(JComponent parent, String name, KeyStroke keyStroke, Action action) {
        InputMap inputMap = parent.getInputMap(WHEN_IN_FOCUSED_WINDOW);
        ActionMap actionMap = parent.getActionMap();

        inputMap.put(keyStroke, name);
        actionMap.put(name, action);
    }

    public static KeyStroke keyStrokeFor(Input key) {
        return keyStrokeFor(key, 0, KeyState.PRESSED);
    }

    public static KeyStroke keyStrokeFor(Input key, int modifiers) {
        return keyStrokeFor(key, modifiers, KeyState.PRESSED);
    }

    public static KeyStroke keyStrokeFor(Input key, int modifiers, KeyState keyState) {
        return key.getKeyStroke(modifiers, keyState);
    }

    public static KeyStroke keyStrokeFor(int key) {
        return keyStrokeFor(key, 0, KeyState.PRESSED);
    }

    public static KeyStroke keyStrokeFor(int key, int modifiers) {
        return keyStrokeFor(key, modifiers, KeyState.PRESSED);
    }

    public static KeyStroke keyStrokeFor(int key, int modifiers, KeyState keyState) {
        return KeyStroke.getKeyStroke(key, 0, keyState == KeyState.RELEASED ? true : false);
    }

}

这处理了很多锅炉 plate/repeating 代码,但也提供了一种方法来“限制”对 API 的可能输入。请注意 Input enum 允许 ENTERTITLE_TOGGLE 作为 keyStrokeFor 方法的可行选项。

请注意,TITLE_TOGGLE“隐藏”了所使用的击键,但同时它也是非常自我记录的。您可以通过许多其他方式来构建这些概念,这只是一个示例。

还有...

public class Button extends JLabel {

会从我这里得到一个非常大的,不,。 API 中已经有一个按钮组件,您应该使用它。它甚至支持诸如翻滚之类的功能, and example,以及许多其他功能,您将花费大量时间 re-building。

参见 How to Use Buttons, Check Boxes, and Radio Buttons。而且,是的,如果您确实需要,您可以删除外观填充背景和边框。


我不了解你,我也不使用 Intellij,但是 return ImageIO.read(new File(GFX_PATH + name)); 对我来说很糟糕。

像这样的资源应该真正嵌入到应用程序的运行时上下文中(“包含在 Jar 中”的技术术语)。当“工作目录”上下文与 res 文件夹的位置不同时,这将防止在运行时尝试定位资源时出现问题。

你应该使用...

return ImageIO.read(getClass().getResource(GFX_PATH + name));

但是我不知道如何配置 Intellji 以将 res 文件夹的上下文包含到您的应用程序上下文中。


public void seeWindow(GameState state){
    // Use a CardLayout
    switch(state){
        case TITLE -> setContentPane(getTitleWindow());
        case MENU -> setContentPane(getMenuWindow());
        case SETTINGS -> setContentPane(getSettingsWindow());
    }
    pack();
}

好吧,目光短浅。相反,您应该使用 CardLayout 来为您执行此操作,并且通常可以可靠地工作。

参见如何使用 CardLayout 了解更多详情