28

If I minimize a JFrame which was Aero-snapped to the left of the screen by clicking on the minimize-button of the Windows WindowDecoration and unminimize it by Alt-Tabbing or clicking it in the Windows TaskBar, the frame gets restored correctly snapped to the left. Good!

But if I minimize the frame by

setExtendedState( getExtendedState() | Frame.ICONIFIED );

and look at the preview by hovering over the Windows TaskBar, it shows the frame a wrong position. After unminimizing it by Alt-Tabbing or clicking it in the Windows TaskBar, the frame gets restored at this wrong position and size. The frame-bounds are the "unsnapped" values, which Windows normally remembers to restore if you drag the frame away from the ScreenBorder.

A screen recording of the Bug:

enter image description here

My conclusion is, that Java does not know about AeroSnap and delivers the wrong bounds to Windows. (For example Toolkit.getDefaultToolkit().isFrameStateSupported( Frame.MAXIMIZED_VERT ) ); returns false.)

This is my fix for the bug:

import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Frame;
import java.awt.Point;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;

/**
 * Fix for the "Frame does not know the AeroSnap feature of Windows"-Bug.
 *
 * @author bobndrew 20160106
 */
public class SwingFrameStateWindowsAeroSnapBug extends JFrame
{
  Point     location = null;
  Dimension size     = null;


  public SwingFrameStateWindowsAeroSnapBug( final String title )
  {
    super( title );
    initUI();
  }

  private void initUI()
  {
    setDefaultCloseOperation( EXIT_ON_CLOSE );
    setLayout( new FlowLayout() );
    final JButton minimize = new JButton( "Minimize" );
    final JButton maximize = new JButton( "Maximize" );
    final JButton normal = new JButton( "Normal" );
    add( normal );
    add( minimize );
    add( maximize );
    pack();
    setSize( 200, 200 );


    final ActionListener listener = actionEvent ->
    {
      if ( actionEvent.getSource() == normal )
      {
        setExtendedState( Frame.NORMAL );
      }
      else if ( actionEvent.getSource() == minimize )
      {
        //Size and Location have to be saved here, before the minimizing of an AeroSnapped WindowsWindow leads to wrong values:
        location = getLocation();
        size = getSize();
        System.out.println( "saving location (before iconify) " + size + " and " + location );

        setExtendedState( getExtendedState() | Frame.ICONIFIED );//used "getExtendedState() |" to preserve the MAXIMIZED_BOTH state

        //does not fix the bug; needs a Window-Drag after DeMinimzing before the size is applied:
        //          setSize( size );
        //          setLocation( location );
      }
      else if ( actionEvent.getSource() == maximize )
      {
        setExtendedState( getExtendedState() | Frame.MAXIMIZED_BOTH );
      }
    };

    minimize.addActionListener( listener );
    maximize.addActionListener( listener );
    normal.addActionListener( listener );

    addWindowStateListener( windowEvent ->
    {
      System.out.println( "oldState=" + windowEvent.getOldState() + "  newState=" + windowEvent.getNewState() );

      if ( size != null && location != null )
      {
        if ( windowEvent.getOldState() == Frame.ICONIFIED )
        {
          System.out.println( "Fixing (possibly) wrong size and location on de-iconifying to " + size + " and " + location + "\n" );
          setSize( size );
          setLocation( location );

          //Size and Location should only be applied once. Set NULL to avoid a wrong DeMinimizing of a following Windows-Decoration-Button-Minimize!
          size = null;
          location = null;
        }
        else if ( windowEvent.getOldState() == (Frame.ICONIFIED | Frame.MAXIMIZED_BOTH) )
        {
          System.out.println( "Set size and location to NULL (old values: " + size + " and " + location + ")" );
          //Size and Location does not have to be applied, Java can handle the MAXIMIZED_BOTH state. Set NULL to avoid a wrong DeMinimizing of a following Windows-Decoration-Button-Minimize!
          size = null;
          location = null;
        }
      }

    } );
  }


  public static void main( final String[] args )
  {
    SwingUtilities.invokeLater( new Runnable()
    {
      @Override
      public void run()
      {
        new SwingFrameStateWindowsAeroSnapBug( "AeroSnap and the Frame State" ).setVisible( true );
      }
    } );
  }
}

This seems to work for all situations under Windows7, but it feels like too much messing around with the window-management. And I avoided to test this under Linux or MacOS for some reason ;-)

Is there a better way to let AeroSnap and Java Frames work together?


Edit:

I've filed a bug at Oracle: http://bugs.java.com/bugdatabase/view_bug.do?bug_id=8147840

bobndrew
  • 395
  • 10
  • 32

2 Answers2

7

Is there a better way to let AeroSnap and Java Frames work together?

Not much better. Directly setting the extended state bypasses the OS's treatment of setting it.

If you take a look at the source code of JFrame#setExtendedState you will see that it calls the FramePeer's setState method. The JDK's JFrame implementation of the FramePeer interface is the WFramePeer class, which declares its setState method as native. So, you're out of luck until Oracle does something about it or you use native code (see below).

Fortunately, you don't necessarily have to go nuts with event listeners and caching bounds. Hiding and showing the frame is enough to "reset" the size to what it was before the minimization:

public class AeroResize extends JFrame {

    public AeroResize(final String title) {

        super(title);
        initUI();
    }

    private void initUI() {

        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setLayout(new FlowLayout());
        final JButton minimize = new JButton("Minimize");
        final JButton maximize = new JButton("Maximize");
        final JButton normal = new JButton("Normal");
        add(normal);
        add(minimize);
        add(maximize);
        pack();

        minimize.addActionListener(e -> {
            setVisible(false);
            setExtendedState(getExtendedState() | JFrame.ICONIFIED);
            setVisible(true);
//          setLocation(getLocationOnScreen()); // Needed only for the preview. See comments section below.
        });
    }

    public static void main(final String[] args) {

        SwingUtilities.invokeLater(() -> new AeroResize("AeroSnap and the Frame State").setVisible(true));
    }
}

Though this does have a side-effect of not giving a detailed preview of the frame's contents:

enter image description here

Solution using native code

If you'd care to use JNA, then you can completely mimic the native platform's minimization. You'll need to include jna.jar and jna-platform.jar in your build path.

import java.awt.FlowLayout;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;

import com.sun.jna.Native;
import com.sun.jna.platform.win32.User32;
import com.sun.jna.platform.win32.WinDef.HWND;

public class AeroResize extends JFrame {

    public AeroResize(final String title) {

        super(title);
        initUI();
    }

    private void initUI() {

        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setLayout(new FlowLayout());
        final JButton minimize = new JButton("Minimize");
        final JButton maximize = new JButton("Maximize");
        final JButton normal = new JButton("Normal");
        add(normal);
        add(minimize);
        add(maximize);
        pack();

        minimize.addActionListener(e -> {
            HWND windowHandle = new HWND(Native.getComponentPointer(AeroResize.this));
            User32.INSTANCE.CloseWindow(windowHandle);
        });
    }

    public static void main(final String[] args) {

        SwingUtilities.invokeLater(() -> new AeroResize("AeroSnap and the Frame State").setVisible(true));
    }
}

It's pretty self explanatory. You get a pointer to the window and use the native CloseWindow (which actually minimizes, go figure) on it. Note that the minimalistic way I wrote it will cause a small delay the first time the button is pressed because the User32 instance is loaded. You can load it on startup to avoid this first-time delay.

Credit goes to the accepted answer here.

Community
  • 1
  • 1
user1803551
  • 12,965
  • 5
  • 47
  • 74
  • Thanks for your Analysis of `WFramePeer`. – bobndrew Jan 13 '16 at 14:40
  • Unfortunately your "Hiding and showing the frame "-sourcecode works only for a snapped Frame-state. If the Frame is in the middle of the Screen before minimizing, it will always be restored to the top-left-corner. And while restoring a snapped-frame correctly, it loses the before-snapped position and size (which normally would be restored on un-snapping). – bobndrew Jan 13 '16 at 14:49
  • @bobndrew "*If the Frame is in the middle of the Screen before minimizing, it will always be restored to the top-left-corner.*" I don't get this behavior. My frame always restores correctly (position and size). – user1803551 Jan 13 '16 at 16:08
  • @bobndrew What are the exact steps you make after the frame is displayed? – user1803551 Jan 13 '16 at 16:16
  • Start your AeroResize.java. The `Frame` is displayed at x=0, y=0. Move the `Frame` to x=400, y=400. Do NOT snap it to an edge. Click the "Minimize"-`JButton`. The TaskBar-preview (with Icon as content) shows the `Frame` at x=0, y=0. Unminimize the `Frame` and it will be displayed at x=0, y=0. The reason is your line `setLocation( getLocationOnScreen() ); // Needed only for the preview` which sets the `Frame` to x=-32000, y=-32000 (which results in x=0, y=0). This also happens with a `Frame` snapped to the right border of the screen. – bobndrew Jan 14 '16 at 09:07
  • If I delete the `setLocation(...)` line, it works for me as expected (with jdk1.8.0_60 and Windows7); only the preview is at the wrong position and has the icon as content. But your `setVisible()`-solution is much easier to use than mine! – bobndrew Jan 14 '16 at 09:15
  • @bobndrew You are right, I was in a multi-monitor environment where the behavior is slightly different. Removing the `setLocation` line makes the preview location of snapped frames to be the non-snapped position prior to snapping. It's an odd trade-off. I'll see if there is something to be done about it. – user1803551 Jan 14 '16 at 15:44
  • 1
    @bobndrew I tried to play with it a bit more. The preview position is too dependent on the native operations, I couldn't reliably make it the correct one. I did add a native code solution, though. – user1803551 Jan 17 '16 at 17:15
  • Your native code solution works as expected! Thank you again and have fun with the bounty!! I have filed a bug at java.com and will post a link here soon. – bobndrew Jan 18 '16 at 14:05
  • @bobndrew Did you file the bug? If so, please post the link in your question or a comment to your question. – user1803551 Feb 03 '16 at 15:26
  • ...I was waiting for a second "bug-accepted" eMail, but Oracle sends only one "submitted" eMail. Anyway, the link to the bug is http://bugs.java.com/bugdatabase/view_bug.do?bug_id=8147840. – bobndrew Feb 04 '16 at 08:57
0

This seems to be a Swing bug. The bug report on the Bug Database:

JDK-7029238 : componentResized not called when the form is snapped

In this report the bug could not be reproduced, now you encountered the same bug (I think it is the same, or related at least), maybe it is a good time to re-open this report. (I did not find any other reference to this, so I assume it hasn´t been fixed)

Mayuso
  • 1,291
  • 4
  • 19
  • 41
  • 1
    Maybe the bugs are related, but the one you've found looks more like the attached screenshot in [JDK-8016356](https://bugs.openjdk.java.net/browse/JDK-8016356). Here's a [video](https://www.youtube.com/watch?v=-nZEh6Oo7Q8) of it. My bug has more to do with _"Loosing the snapped-state by programmatically minimizing"_. – bobndrew Jan 11 '16 at 13:56
  • Oh, ok, sorry then. I guess my english is worse than I thought. – Mayuso Jan 11 '16 at 14:01