How To create A Large Size Custom Cursor In Java?

2019-07-29 06:53发布

问题:

I'm developing a Java Swing app for an award winning password protection system, and I need a large custom cursor [ 80 x 80 ], you might ask why so large, there is an online web demo you may look at to learn why it needs to be so large : http://gatecybertech.net

That large cursor is used on the login page in the above link. Of course you need to create a test password first before you can try the login process.

But anyway, in my Swing app, I hit a limit of 32 x 32 for the largest possible custom cursor, my code looks like the following :

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

The image size of Cursor_Crosshair.PNG is : 80 x 80

But what shows up in the screen is a shrinked version of it at : 32 x 32

So my question is : how can I bypass the size limit on customer cursor image, and make the cursor to show up at the size of 80 x 80 ?

I know the OS might be the reason for the limit, is there a way to overcome it ?

回答1:

Here's my take on the glass pane painting approach. This is set up to behave pretty much like setting a custom cursor. The default "arrow" cursor is hidden while the custom cursor is shown, and the custom cursor is hidden when a component has some other cursor set, such as a text box.

Unfortunately, it ended up seeming to require quite a bit of Swing black magic, so I don't like it very much, but it does seem to work correctly. I've done a cursor like this before, but it was for something simpler, so I didn't run in to these issues.

Some of the problems I ran in to are:

  • The glass pane intercepts cursor changes (described e.g. on 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), but why that works and doesn't seem to break anything else is mysterious.

  • Mouse exit events sometimes return a location inside the bounds of the component, so I don't think there's a reliable way to know when the mouse leaves the window except to use a timer.

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


回答2:

OK, after some research and modification, I found the answer :

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

And the Cursor_Crosshair.PNG is like this :