如何防止 TrayIcon 弹出窗口占用整个调度程序线程

How to prevent the TrayIcon popup to occupy the whole dispatcher thread

我有一个使用 JFrameTrayIcon 的 java 应用程序,我向 TrayIcon 添加了一个 PopupMenu

当我点击 TrayIcon 时弹出菜单出现,但只要 PopupMenu 可见,主框架就会冻结。

我的第一个想法是事件派发线程被人占用了。所以 我写了一个使用 swing worker 和进度条的小示例应用程序。

public class TrayIconTest {

  public static void main(String[] args) throws AWTException {
    JFrame frame = new JFrame("TrayIconTest");
    frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

    Container contentPane = frame.getContentPane();
    JProgressBar jProgressBar = new JProgressBar();
    BoundedRangeModel boundedRangeModel = jProgressBar.getModel();
    contentPane.add(jProgressBar);

    boundedRangeModel.setMinimum(0);
    boundedRangeModel.setMaximum(100);

    PopupMenu popup = new PopupMenu();
    TrayIcon trayIcon =
            new TrayIcon(new BufferedImage(64, 64, BufferedImage.TYPE_INT_RGB), "TEST");


    // Create a popup menu components
    MenuItem aboutItem = new MenuItem("About");
    popup.add(aboutItem);
    trayIcon.setPopupMenu(popup);
    SystemTray tray = SystemTray.getSystemTray();
    tray.add(trayIcon);

    IndeterminateSwingWorker indeterminateSwingWorker = new IndeterminateSwingWorker(boundedRangeModel);
    indeterminateSwingWorker.execute();


    frame.setSize(640, 80);
    frame.setLocationRelativeTo(null);
    frame.setVisible(true);

  }
}

class IndeterminateSwingWorker extends SwingWorker<Void, Integer> {

  private BoundedRangeModel boundedRangeModel;

  public IndeterminateSwingWorker(BoundedRangeModel boundedRangeModel) {
    this.boundedRangeModel = boundedRangeModel;
  }

  @Override
  protected Void doInBackground() throws Exception {
    int i = 0;
    long start = System.currentTimeMillis();
    long runtime = TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES);
    while (true) {
      i++;
      i %= boundedRangeModel.getMaximum();

      publish(i);
      Thread.sleep(20);

      long now = System.currentTimeMillis();
      if ((now - start) > runtime) {
        break;
      }

      System.out.println("i = " + i);
    }
    return null;
  }

  @Override
  protected void process(List<Integer> chunks) {
    for (Integer chunk : chunks) {
      boundedRangeModel.setValue(chunk);
    }
  }
}

如何重现

当您启动示例应用程序时,您将看到一个带有进度条的框架。 A SwingWorker 模拟一个进度动作。

SwingWorker发布进度值,同时打印到System.out

当我单击托盘图标 ('the black one') 时,进度条冻结并显示弹出菜单。我可以看到 SwingWorker 仍然在后台 运行,因为它将进度值输出到命令行。

调查

所以我查看了使用 jvisualvm 的应用程序,它告诉我只要弹出窗口可见,事件调度程序线程就非常忙。

我发现通过 sun.awt.windows.WTrayIconPeer.showPopupMenu(int, int).

方法可以看到弹出窗口

此方法使用 EventQueue.invokeLater 向事件分派线程提交一个可运行对象。在这个 runnable 中,方法 sun.awt.windows.WPopupMenuPeer._show 被调用。这个方法native而且好像实现了一个繁忙的等待循环,以至于事件派发线程完全被这个方法占用了。 因此,只要弹出菜单可见,其他 ui 相关任务就不会执行。

有谁知道在显示 TrayIcon 弹出窗口时保持事件分派线程响应的解决方法?

我现在使用 JPopupMenu 作为解决方法,但您不能将 JPopupMenu 直接添加到 TrayIcon

诀窍是写一个MouseListener

public class JPopupMenuMouseAdapter extends MouseAdapter {

  private JPopupMenu popupMenu;

  public JPopupMenuMouseAdapter(JPopupMenu popupMenu) {
    this.popupMenu = popupMenu;
  }

  @Override
  public void mouseReleased(MouseEvent e) {
    maybeShowPopup(e);
  }

  @Override
  public void mousePressed(MouseEvent e) {
    maybeShowPopup(e);
  }

  private void maybeShowPopup(MouseEvent e) {
    if (e.isPopupTrigger()) {
      Dimension size = popupMenu.getPreferredSize();
      popupMenu.setLocation(e.getX() - size.width, e.getY() - size.height);
      popupMenu.setInvoker(popupMenu);
      popupMenu.setVisible(true);
    }
  }

并将其连接到 TrayIcon

TrayIcon trayIcon = ...;
JPopupMenu popupMenu = ...;

JPopupMenuMouseAdapter jPopupMenuMouseAdapter = new JPopupMenuMouseAdapter(popupMenu);
trayIcon.addMouseListener(jPopupMenuMouseAdapter);