在Windows 10中,能否为AWT应用程序提供清晰的任务栏图标?

15
我正在尝试设置Java AWT应用程序的图标,以便在Windows 10任务栏上以本地分辨率呈现(即使桌面缩放设置高于100%时也是如此)。默认情况下,如果可执行文件嵌入包含多个大小的图标,则似乎Windows会选择一个比任务栏图标实际大小更大的大小并将其缩小(在100%缩放时,它将32像素图标调整为24个像素,即使提供了24像素图标,对于其他比例也是如此)。
我已通过仅加载正确大小的图标作为资源并向窗口发送WM_SETICON消息来解决了C ++ MFC应用程序的此问题,这将在任务栏和alt-tab对话框中产生清晰的图标。
smallIcon = (HICON)LoadImage( myInstance, MAKEINTRESOURCE(smallIconRes), IMAGE_ICON, smallIconSize, smallIconSize, LR_DEFAULTCOLOR );
SendMessage(hWnd, WM_SETICON, ICON_SMALL, (LPARAM)smallIcon);

bigIcon   = (HICON)LoadImage( myInstance, MAKEINTRESOURCE(bigIconRes),   IMAGE_ICON, bigIconSize,   bigIconSize,   LR_DEFAULTCOLOR );
SendMessage(hWnd, WM_SETICON, ICON_BIG,   (LPARAM)bigIcon); 

这种方法似乎不适用于Java应用程序——将wParam设置为ICON_SMALL的WM_SETICON消息可正常工作,但是相应的ICON_BIG则会被忽略。

如果我尝试使用Java的API来设置图标,可以通过以下方式实现:

    List<Image> icons = new ArrayList<Image>();
    icons.add(windowIcons.getIcon(20)); // small icons are 20x20 pixels
    icons.add(windowIcons.getIcon(30)); // large are 30x30 at 125% scale
    setIconImages(icons);

使用了正确的图标,但它看起来模糊,好像有些东西将其调整为“预期”大小,然后再次调整回来。左侧是它的外观,右侧是图标文件的内容。

Java application's icon vs. how it should look

那么,我的问题是:在这个Java应用程序中,我可以做些什么来让Windows在任务栏上呈现我提供的图标而不需要缩放和模糊细节?


1
请参阅 Swing 中使用的框架图标大小 - Andrew Thompson
2个回答

4
sun.awt.SunToolkit中确实有一个名为getScaledIconImage()的缩放功能,无论何时设置图标时都会使用该函数。您必须绕过此函数才能获得未走样的图标。因此,您需要一个替代java.awt.Window.setIconImages()方法的解决方案。
提供了几个图标图像,如Icon16x16.png,Icon24x24.png等。这是一个示例customSetIconImages()方法,将一个清晰的24x24像素图标放在Windows 10任务栏中。
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import javax.swing.ImageIcon;
import java.awt.peer.WindowPeer;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;

@SuppressWarnings("serial")
public class MyFrame extends Frame implements WindowListener {

    final Image i16, i24, i32, i48;

    MyFrame() throws Exception {

        i16 = Toolkit.getDefaultToolkit().getImage("Icon16x16.png");
        i24 = Toolkit.getDefaultToolkit().getImage("Icon24x24.png");
        i32 = Toolkit.getDefaultToolkit().getImage("Icon32x32.png");
        i48 = Toolkit.getDefaultToolkit().getImage("Icon48x48.png");

        addWindowListener(this);
        setSize(500,300);
        setTitle("Unaliased icon example");
        setLayout(new FlowLayout());
        setVisible(true);
    }

    public synchronized void customSetIconImages(java.util.List<Image> icons) throws Exception {
        Field windowIcons = Class.forName("java.awt.Window").getDeclaredField("icons");
        windowIcons.setAccessible(true);
        windowIcons.set(this, new ArrayList<Image>(icons));

        if (getPeer() != null)
            updateIconImages(i24, 24, 24, i24, 24, 24);

        firePropertyChange("iconImage", null, null);
    }

    public void updateIconImages(Image big, int bw, int bh, Image small, int sw, int sh) throws Exception {
        DataBufferInt iconData = getUnscaledIconData(big, bw, bh);
        DataBufferInt iconSmData = getUnscaledIconData(small, sw, sh);

        WindowPeer peer = (WindowPeer) getPeer();
        Method setIconImagesData = Class.forName("sun.awt.windows.WWindowPeer").getDeclaredMethod("setIconImagesData", int[].class, int.class, int.class, int[].class, int.class, int.class);
        setIconImagesData.setAccessible(true);
        setIconImagesData.invoke(peer, iconData.getData(), bw, bh, iconSmData.getData(), sw, sh);
    }

    public static DataBufferInt getUnscaledIconData(Image image, int w, int h) {
        Image temporary = new ImageIcon(image).getImage();
        BufferedImage buffImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2d = buffImage.createGraphics();
        g2d.drawImage(temporary, 0, 0, null);
        g2d.dispose();
        Raster raster = buffImage.getRaster();
        DataBuffer buffer = raster.getDataBuffer();
        return (DataBufferInt) buffer;
    }

    @Override
    public void windowOpened(WindowEvent arg0) {
        try {
            customSetIconImages(Arrays.asList(i24));
        } catch (Exception e) {
            System.err.println(e.getClass().getName()+" "+e.getMessage());
        }
    }

    @Override
    public void windowActivated(WindowEvent arg0) {
    }

    @Override
    public void windowClosed(WindowEvent arg0) {
    }

    @Override
    public void windowClosing(WindowEvent arg0) {
        dispose();
    }

    @Override
    public void windowDeactivated(WindowEvent arg0) {
    }

    @Override
    public void windowDeiconified(WindowEvent arg0) {
    }

    @Override
    public void windowIconified(WindowEvent arg0) {
    }

    public static void main(String args[]) throws Exception {
        MyFrame fr = new MyFrame();
    }
}

正如@df778899所说,sun.awt.windows.WWindowPeer内部有四个私有的本地方法,您可以调用它们以确定系统图标的大小。您可以将这些方法返回的信息与自己的版本getScaledIconImage()结合使用,该版本根据您的需求执行unaliasing或不执行。

最后,请注意,这是一种非常肮脏的hack,仅用于获取未对齐的图标。我只在Java 8和Windows 10中进行了测试。很有可能在更新版本的Java中无法正常工作。


1
谢谢Serg - 这正是我在寻找的,我已经尝试了它,它运行得非常好!有一个问题:customSetIconImages函数的第一部分似乎是以通用方式设置窗口图标,而您的代码在没有它的情况下也可以正常工作。它是用于无法使用updateIconImages(例如如果getPeer不可用)的情况下的备选方案,还是它是您代码中重要的一部分,只是我没有看到需要它的情况? - MLdeS
1
java.awt.Window的标准setIconImages()方法设置内部List<Image>图标。我还没有深入研究Sun的代码,无法确定它的作用。因此,在编写备选方法时,我决定也初始化图标列表,以防万一... - Serg M Ten
请注意,JDK 在选择 2 张图像时并没有错,Windows 只允许“大”和“小”图标(参见 WM_SETICON),而 JDK 会选择最接近的尺寸,并在图像不正确的情况下进行缩放。问题似乎出在 JDK 确定正确尺寸的地方,它使用 DPI 进行调整。一旦尺寸不正确,它就会向 Windows 提供错误大小的图标,而 Windows 会对其进行缩放,看起来很糟糕。这种情况发生在 150% DPI 的情况下。https://github.com/openjdk/jdk/blob/62a235429931f05f1167bcd92e57243f87ab3f74/src/java.desktop/windows/native/libawt/windows/awt_Window.cpp#L3659-L3669 - NateS
另外需要注意的是,正确的加载和设置图标的方法不涉及应用任何 DPI:https://blog.barthe.ph/2009/07/17/wmseticon/ - NateS
原帖作者知道他想要一个未缩放的24x24图标。但是我如何在运行时确定特定用户工具栏的最佳图标大小,以及他们拥有的任何显示设置? - Enwired

2
这可能不是您希望得到的答案,但这看起来像是JDK层面的问题。
窗口图标由sun.awt.windows.WWindowPeer类处理,该类又进行了一些本地方法调用,但在源代码中有足够的内容指向问题。请阅读此处的重要部分。
基本上,无论提供多少个图标图像尺寸,它只会选择两个尺寸-用于WWindowPeer.getSysIconWidth()getSysSmIconWidth() - 传递到本地setIconImagesData()方法中。 getSysIconWidth()getSysSmIconWidth()方法也是本地的,但可以直接检查它们的返回值:
JFrame frame = new JFrame();
runOnPeer(frame, "getSysIconWidth");
runOnPeer(frame, "getSysIconHeight");
runOnPeer(frame, "getSysSmIconWidth");
runOnPeer(frame, "getSysSmIconHeight");

private void runOnPeer(JFrame frame, String methodName) {

    //JDK8 style
    //ComponentPeer peer = frame.getPeer();

    //JDK11 style
    Field peerField = Component.class.getDeclaredField("peer");
    peerField.setAccessible(true);
    Object peer = peerField.get(frame);

    Method method = Class.forName("sun.awt.windows.WWindowPeer")
            .getDeclaredMethod(methodName);
    method.setAccessible(true);
    System.out.println(methodName + "()=" + method.invoke(peer));
}

"...在Windows 10上返回以下内容..."
getSysIconWidth()=32
getSysIconHeight()=32
getSysSmIconWidth()=16
getSysSmIconHeight()=16

正如你所说,显然其中一个图片尺寸正在被缩放用于任务栏。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接