如何在 Java 中创建大尺寸自定义光标?

How To create A Large Size Custom Cursor In Java?

我正在为屡获殊荣的密码保护系统开发 Java Swing 应用程序,我需要一个大的自定义光标 [80 x 80],您可能会问为什么这么大,网上有一个您可以查看演示以了解为什么它需要这么大:http://gatecybertech.net

上面link中的登录页面使用了那个大光标。当然,您需要先创建一个测试密码,然后才能尝试登录。

但无论如何,在我的 Swing 应用程序中,我将最大可能的自定义光标限制为 32 x 32,我的代码如下所示:

Image cursorImage = toolkit.getImage("Cursor_Crosshair.PNG");
Tabular_Panel.setCursor(Toolkit.getDefaultToolkit().createCustomCursor(cursorImage,new Point(0,0),"custom cursor"));

Cursor_Crosshair.PNG 的图像大小为:80 x 80

但屏幕上显示的是它的缩小版:32 x 32

所以我的问题是:如何绕过客户光标图像的大小限制,并使光标以 80 x 80 的大小显示?

我知道 OS 可能是限制的原因,有没有办法克服它?

好的,经过一番研究和修改,我找到了答案:

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.io.*;
import javax.imageio.*;
import javax.swing.event.MouseInputAdapter;

public class Demo_Large_Custom_Cursor
{
  static private MyGlassPane myGlassPane;

  // Create the GUI and show it. For thread safety, this method should be invoked from the event-dispatching thread.
  private static void createAndShowGUI()
  {
    //Create and set up the window.
    JFrame frame=new JFrame("Demo_Large_Custom_Cursor");
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    //Start creating and adding components.
    JCheckBox changeButton=new JCheckBox("Custom Cursor \"visible\"");
    changeButton.setSelected(false);

    //Set up the content pane, where the "main GUI" lives.
    Container contentPane=frame.getContentPane();
    contentPane.setLayout(new FlowLayout());
    contentPane.add(changeButton);

    JButton Button_1=new JButton("<Html><Table Cellpadding=7><Tr><Td>A</Td><Td>B</Td></Tr><Tr><Td>C</Td><Td>D</Td></Tr></Table></Html>");
    Button_1.setPreferredSize(new Dimension(80,80));
    Button_1.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { Out("Button 1"); } });
    contentPane.add(Button_1);

    JButton Button_2=new JButton("<Html><Table Cellpadding=7><Tr><Td>1</Td><Td>2</Td></Tr><Tr><Td>3</Td><Td>4</Td></Tr></Table></Html>");
    Button_2.setPreferredSize(new Dimension(80,80));
    Button_2.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { Out("Button 2"); } });
    contentPane.add(Button_2);

    //Set up the menu bar, which appears above the content pane.
    JMenuBar menuBar=new JMenuBar();
    JMenu menu=new JMenu("Menu");
    menu.add(new JMenuItem("Do nothing"));
    menuBar.add(menu);
    frame.setJMenuBar(menuBar);

    //Set up the glass pane, which appears over both menu bar
    //and content pane and is an item listener on the change
    //button.
    myGlassPane=new MyGlassPane(changeButton,menuBar,frame.getContentPane());
    changeButton.addItemListener(myGlassPane);
    frame.setGlassPane(myGlassPane);

    //Show the window.
    frame.setLocationRelativeTo(null);
    frame.pack();
    frame.setVisible(true);
  }

  private static void out(String message) { System.out.print(message); }

  private static void Out(String message) { System.out.println(message); }

  public static void main(String[] args)
  {
    //Schedule a job for the event-dispatching thread:
    //creating and showing this application's GUI.
    javax.swing.SwingUtilities.invokeLater(new Runnable()
    {
      public void run()
      {
        createAndShowGUI();
      }
    });
  }
}

/**
 We have to provide our own glass pane so that it can paint.
 */
class MyGlassPane extends JComponent implements ItemListener
{
  Point point;

  //React to change button clicks.
  public void itemStateChanged(ItemEvent e)
  {
    setVisible(e.getStateChange()==ItemEvent.SELECTED);
  }

  protected void paintComponent(Graphics g)
  {
    try
    {
    if (point!=null)
    {
//      g.setColor(Color.red);
//      g.fillOval(point.x-10,point.y-10,20,20);

      BufferedImage image=ImageIO.read(new File("C:/Cursor_Crosshair.PNG"));
      g.drawImage(image,point.x-39,point.y-39,null);
    }
    }
    catch (Exception e) { }
  }

  public void setPoint(Point p)
  {
    point=p;
  }

  public MyGlassPane(AbstractButton aButton,JMenuBar menuBar,Container contentPane)
  {
    CBListener listener=new CBListener(aButton,menuBar,this,contentPane);
    addMouseListener(listener);
    addMouseMotionListener(listener);
  }
}

/**
 Listen for all events that our check box is likely to be interested in. Redispatch them to the check box.
 */
class CBListener extends MouseInputAdapter
{
  Toolkit toolkit;
  Component liveButton;
  JMenuBar menuBar;
  MyGlassPane glassPane;
  Container contentPane;

  public CBListener(Component liveButton,JMenuBar menuBar,MyGlassPane glassPane,Container contentPane)
  {
    toolkit=Toolkit.getDefaultToolkit();
    this.liveButton=liveButton;
    this.menuBar=menuBar;
    this.glassPane=glassPane;
    this.contentPane=contentPane;
  }

  public void mouseMoved(MouseEvent e)
  {
//    redispatchMouseEvent(e,false);
    redispatchMouseEvent(e,true);
  }

  public void mouseDragged(MouseEvent e)
  {
    redispatchMouseEvent(e,false);
  }

  public void mouseClicked(MouseEvent e)
  {
    redispatchMouseEvent(e,false);
  }

  public void mouseEntered(MouseEvent e)
  {
    redispatchMouseEvent(e,false);
  }

  public void mouseExited(MouseEvent e)
  {
    redispatchMouseEvent(e,false);
  }

  public void mousePressed(MouseEvent e)
  {
    redispatchMouseEvent(e,false);
  }

  public void mouseReleased(MouseEvent e)
  {
    redispatchMouseEvent(e,true);
  }

  //A basic implementation of redispatching events.
  private void redispatchMouseEvent(MouseEvent e,boolean repaint)
  {
    Point glassPanePoint=e.getPoint();
    Container container=contentPane;
    Point containerPoint=SwingUtilities.convertPoint(glassPane,glassPanePoint,contentPane);
    if (containerPoint.y<0)
    { //we're not in the content pane
      if (containerPoint.y+menuBar.getHeight()>=0)
      {
        //The mouse event is over the menu bar.
        //Could handle specially.
      }
      else
      {
        //The mouse event is over non-system window 
        //decorations, such as the ones provided by
        //the Java look and feel.
        //Could handle specially.
      }
    }
    else
    {
      //The mouse event is probably over the content pane.
      //Find out exactly which component it's over.  
      Component component=SwingUtilities.getDeepestComponentAt(container,containerPoint.x,containerPoint.y);

//      if ((component!=null) && (component.equals(liveButton)))
      if ((component!=null))
      {
        //Forward events over the check box.
        Point componentPoint=SwingUtilities.convertPoint(glassPane,glassPanePoint,component);
        component.dispatchEvent(new MouseEvent(component,e.getID(),e.getWhen(),e.getModifiers(),componentPoint.x,componentPoint.y,e.getClickCount(),e.isPopupTrigger()));
      }
    }

    //Update the glass pane if requested.
    if (repaint)
    {
      glassPane.setPoint(glassPanePoint);
      glassPane.repaint();
    }
  }
}

而 Cursor_Crosshair.PNG 是这样的:

这是我对玻璃板绘画方法的看法。这被设置为表现得非常像设置自定义光标。默认 "arrow" 光标在显示自定义光标时隐藏,当组件具有其他光标集(例如文本框)时自定义光标隐藏。

不幸的是,它最终似乎需要相当多的 Swing 黑魔法,所以我不太喜欢它,但它似乎工作正常。我以前做过这样的游标,但它是为了更简单的东西,所以我没有 运行 涉及这些问题。

我 运行 遇到的一些问题是:

  • 玻璃窗格拦截光标更改(例如在 SO here). The only solution I've been able to find is to override Component.contains(int,int) to return false (described here, shown here 上描述),但为什么它有效并且似乎没有破坏其他任何东西是神秘的。

  • 鼠标退出事件有时 return 组件边界内的一个位置,所以我认为没有可靠的方法知道鼠标何时离开 window除了使用计时器。

package mcve;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import javax.imageio.*;
import java.net.*;
import java.io.*;

public class LargeCursor {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            JFrame frame = new JFrame();

            JPanel glass = new CustomGlassPane();
            glass.add(new CursorPanel(), BorderLayout.CENTER);
            frame.setGlassPane(glass);
            // This next call is necessary because JFrame.setGlassPane delegates to the root pane:
            // - https://docs.oracle.com/javase/9/docs/api/javax/swing/RootPaneContainer.html#setGlassPane-java.awt.Component-
            // - http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/classes/javax/swing/JFrame.java#l738
            // And JRootPane.setGlassPane may call setVisible(false):
            // - https://docs.oracle.com/javase/9/docs/api/javax/swing/JRootPane.html#setGlassPane-java.awt.Component-
            // - http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/classes/javax/swing/JRootPane.java#l663
            glass.setVisible(true);

            JPanel content = createTestPanel();
            content.setCursor(BlankCursor.INSTANCE);

            frame.setContentPane(content);
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setVisible(true);
        });
    }

    static class CustomGlassPane extends JPanel {
        CustomGlassPane() {
            super(new BorderLayout());
            super.setOpaque(false);
        }
        @Override
        public boolean contains(int x, int y) {
            return false;
        }
    }

    static class CursorPanel extends JPanel {
        final BufferedImage cursorImage;
        Point mouseLocation;

        CursorPanel() {
            try {
                cursorImage = createTransparentImage(
                    ImageIO.read(new URL("https://i.stack.imgur.com/9h2oI.png")));
            } catch (IOException x) {
                throw new UncheckedIOException(x);
            }

            setOpaque(false);

            long mask = AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK;

            Toolkit.getDefaultToolkit().addAWTEventListener((AWTEvent e) -> {
                switch (e.getID()) {
                    case MouseEvent.MOUSE_ENTERED:
                    case MouseEvent.MOUSE_EXITED:
                    case MouseEvent.MOUSE_MOVED:
                    case MouseEvent.MOUSE_DRAGGED:
                        capturePoint((MouseEvent) e);
                        break;
                }
            }, mask);

            // This turned out to be necessary, because
            // the 'mouse exit' events don't always have
            // a Point location which is outside the pane.
            Timer timer = new Timer(100, (ActionEvent e) -> {
                if (mouseLocation != null) {
                    Point p = MouseInfo.getPointerInfo().getLocation();
                    SwingUtilities.convertPointFromScreen(p, this);
                    if (!contains(p)) {
                        setMouseLocation(null);
                    }
                }
            });
            timer.setRepeats(true);
            timer.start();
        }

        void capturePoint(MouseEvent e) {
            Component comp = e.getComponent();
            Point onThis = SwingUtilities.convertPoint(comp, e.getPoint(), this);
            boolean drawCursor = contains(onThis);

            if (drawCursor) {
                Window window = SwingUtilities.windowForComponent(this);
                if (window instanceof JFrame) {
                    Container content = ((JFrame) window).getContentPane();
                    Point onContent = SwingUtilities.convertPoint(comp, e.getPoint(), content);
                    Component deepest = SwingUtilities.getDeepestComponentAt(content, onContent.x, onContent.y);
                    if (deepest != null) {
                        if (deepest.getCursor() != BlankCursor.INSTANCE) {
                            drawCursor = false;
                        }
                    }
                }
            }

            setMouseLocation(drawCursor ? onThis : null);
        }

        void setMouseLocation(Point mouseLocation) {
            this.mouseLocation = mouseLocation;
            repaint();
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);

            if (mouseLocation != null) {
                int x = mouseLocation.x - (cursorImage.getWidth() / 2);
                int y = mouseLocation.y - (cursorImage.getHeight() / 2);

                g.drawImage(cursorImage, x, y, this);
            }
        }
    }

    static final class BlankCursor {
        static final Cursor INSTANCE =
            Toolkit.getDefaultToolkit().createCustomCursor(
                new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB),
                new Point(),
                "BLANK");
    }

    static JPanel createTestPanel() {
        JPanel panel = new JPanel(new GridLayout(3, 3));
        panel.setBorder(BorderFactory.createEmptyBorder(100, 100, 100, 100));

        for (int i = 0; i < 9; ++i) {
            if ((i % 2) == 0) {
                JTextField field = new JTextField("Text Field");
                field.setHorizontalAlignment(JTextField.CENTER);
                panel.add(field);
            } else {
                panel.add(new JButton("Button"));
            }
        }

        return panel;
    }

    static BufferedImage createTransparentImage(BufferedImage img) {
        BufferedImage copy =
            GraphicsEnvironment.getLocalGraphicsEnvironment()
                               .getDefaultScreenDevice()
                               .getDefaultConfiguration()
                               .createCompatibleImage(img.getWidth(),
                                                      img.getHeight(),
                                                      Transparency.TRANSLUCENT);
        for (int x = 0; x < img.getWidth(); ++x) {
            for (int y = 0; y < img.getHeight(); ++y) {
                int rgb = img.getRGB(x, y) & 0x00FFFFFF;
                int bright = (((rgb >> 16) & 0xFF) + ((rgb >> 8) & 0xFF) + (rgb & 0xFF)) / 3;
                int alpha = 255 - bright;
                copy.setRGB(x, y, (alpha << 24) | rgb);
            }
        }

        return copy;
    }
}