Reputation: 2364
I'm writing a sort of web applet emulator. I read a web page, find the applet parameters, download the applet and run it. It is very important that the applet runs in its own process (i.e. not the emulator process). It should, however, render in the emulator process window.
How does the Java plugin do it? When the separate_jvm
flag is set, the plugin loads the applet in a separate JVM process but the applet still appears in the same browser panel.
I've made some progress by creating a loader class that, on another JVM, adds the target Applet to an undecorated, invisible frame and messages the frame's window handle to the emulator JVM. The latter binds it to a Canvas
instance with user32.SetParent
via JNA, and the display works perfectly.
However, only mouse events are being sent: keyboard input is not forwarded. The applet reports Component#isFocusOwner
as false, and requestFocusInWindow
does not make it the focus owner, returning false. How can I pass keyboard focus to the Applet window handle? My current approach involves a server (the emulator), which receives window handles from the client (the applet). Only mouse events appear to work, since the Applet cannot gain focus.
The server class handles the display of the applet.
import com.sun.jna.*;
import com.sun.jna.platform.win32.User32;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.net.*;
import static com.sun.jna.platform.win32.User32.*;
public class Loader {
private static final String APPLET_DIRECTORY = ""; // TODO: Set this to the directory containing the compiled applet
private static ServerSocket serverSocket;
private static JFrame frame;
private static Canvas nativeDisplayCanvas;
public static void main(String[] argv) throws Exception {
nativeDisplayCanvas = new Canvas();
frame = new JFrame("Frame redirect");
frame.setLayout(new BorderLayout());
frame.add(nativeDisplayCanvas, BorderLayout.CENTER);
frame.setSize(300, 200);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
(new Thread() {
public void run() {
try {
serve();
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
spawnAltJVM(APPLET_DIRECTORY, "AppletDemo");
}
public static void serve() throws Exception {
serverSocket = new ServerSocket(6067);
serverSocket.setSoTimeout(10000);
while (true) {
try {
System.out.println("Waiting for applet on port " + serverSocket.getLocalPort() + "...");
Socket server = serverSocket.accept();
System.out.println("Connected to " + server.getRemoteSocketAddress());
BufferedReader in = new BufferedReader(new InputStreamReader(server.getInputStream()));
DataOutputStream out = new DataOutputStream(server.getOutputStream());
while (true) {
String msg = in.readLine();
if (msg != null && msg.startsWith("child_hwnd")) {
windowCreatedHandler(msg);
out.writeUTF("hwnd_recv\n");
out.flush();
}
}
} catch (IOException ex) {
System.out.println("Something happened to the socket...");
break;
}
}
}
public static void windowCreatedHandler(String message) {
String[] tokens = message.split(":");
final User32 user32 = User32.INSTANCE;
HWND child_applet = new HWND(Pointer.createConstant(Long.parseLong(tokens[1])));
final HWND child_frame = new HWND(Pointer.createConstant(Long.parseLong(tokens[2])));
frame.addComponentListener(
new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
user32.SetWindowPos(child_frame, new HWND(Pointer.NULL), 0, 0, frame.getWidth(), frame.getHeight(), 0);
}
}
);
HWND parent = new HWND(Native.getComponentPointer(nativeDisplayCanvas));
user32.SetParent(child_applet, parent);
int style = user32.GetWindowLong(child_frame, GWL_STYLE) & ~WS_POPUP | (WS_CHILD | WS_VISIBLE);
user32.SetWindowLong(child_applet, GWL_STYLE, style);
user32.SetWindowPos(child_applet, new HWND(Pointer.NULL), 0, 0, nativeDisplayCanvas.getWidth(), nativeDisplayCanvas.getHeight(), 0);
}
public static void spawnAltJVM(String cp, String clazz) throws IOException, InterruptedException, ClassNotFoundException {
ProcessBuilder processBuilder = new ProcessBuilder(System.getProperty("java.home") + File.separator + "bin" + File.separator + "java", "-cp", cp, clazz);
Process applet = processBuilder.start();
final BufferedReader in = new BufferedReader(new InputStreamReader(applet.getInputStream()));
final BufferedReader err = new BufferedReader(new InputStreamReader(applet.getErrorStream()));
(new Thread() {
public void run() {
while (true) {
try {
System.out.println("[client] " + in.readLine());
} catch (IOException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
Meanwhile, the client class just instantiates and messages the handles.
import sun.awt.windows.WComponentPeer;
import javax.swing.*;
import java.applet.Applet;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.net.Socket;
import java.util.concurrent.LinkedBlockingDeque;
public class AppletDemo extends Applet {
private Canvas canvas;
private static Color backgroundColor = Color.RED;
public AppletDemo() {
setLayout(new BorderLayout());
canvas = new Canvas();
add(canvas, BorderLayout.CENTER);
setBackground(Color.CYAN);
canvas.addKeyListener(new KeyAdapter() {
@Override
public void keyTyped(KeyEvent e) {
refreshColors();
}
});
canvas.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
refreshColors();
}
});
}
private void refreshColors() {
SwingUtilities.invokeLater(
new Runnable() {
@Override
public void run() {
backgroundColor = (backgroundColor == Color.RED ? Color.GREEN : Color.RED);
canvas.setBackground(backgroundColor);
}
}
);
}
public static void main(String[] argv) throws Exception {
System.setErr(System.out);
final AppletDemo app = new AppletDemo();
Frame frame = new Frame("AppletViewer");
frame.setLayout(new BorderLayout());
frame.add(app, BorderLayout.CENTER);
frame.setUndecorated(true);
frame.pack(); // Create the native peers
frame.setSize(300, 200);
final Socket client = new Socket("localhost", 6067);
final LinkedBlockingDeque<String> messageQueue = new LinkedBlockingDeque<>();
final DataOutputStream out = new DataOutputStream(client.getOutputStream());
final BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
(new Thread() {
public void run() {
while (true) {
try {
out.writeBytes(messageQueue.take() + "\n");
out.flush();
} catch (IOException | InterruptedException ex) {
ex.printStackTrace();
}
}
}
}).start();
(new Thread() {
public void run() {
while (true) {
try {
if ("hwnd_recv".equals(in.readLine())) {
// Attempt to grab focus in the other process' frame
System.out.println("Trying to request focus...");
System.out.println(app.requestFocusInWindow());
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}).start();
messageQueue.add("child_hwnd:" + ((WComponentPeer) app.getPeer()).getHWnd() + ":" + ((WComponentPeer) frame.getPeer()).getHWnd());
}
}
They're both a bit lengthy because they require some socket work, but they are compilable and should demonstrate the issue. They require JNA to compile. I've shortened them as much as possible at the cost of some good practices.
When Loader
is ran, a window redirecting the AppletDemo
's canvas should appear. Mouse events are sent: the canvas toggles between red and green on a mouse press. Ideally, the same behavior should occur for keystrokes too.
I've used WinSpy to get the handles of a notepad.exe window and text pane, and hardcoding the handles into Loader
. Keyboard focus works perfectly with the multiline edit control, but not with the toplevel window itself. Why? Is this related to the issue I'm having?
I opened up a Chrome window running an applet in WinSpy, and found that the plugin creates no dummy Frame
— the applet canvas is directly set as a child of Chrome. However, I haven't been able to create a native peer for the Applet
, since it seems to require it to be displayable.
I've read about the dangers of cross-process parent/child or owner/owned window relationship, but I can't think of a better way to graft the child applet into the emulator.
Upvotes: 3
Views: 991
Reputation: 1985
With JNA it is as easy as
HWND hwnd1 = User32.INSTANCE.FindWindow(null, "JFrame1");
HWND hwnd2 = User32.INSTANCE.FindWindow(null, "JFrame2");
HWND hwnd3 = User32.INSTANCE.SetParent(hwnd2, hwnd1);
see also
Good or evil - SetParent() win32 API between different processes
Upvotes: 0
Reputation: 16
It looks like you are trying to pass the event to the Canvas object, which you do not explicitly setFocusable(true) for.
If this is the case, then in your AppletDemo constructor, try:
canvas.setFocusable(true);
canvas.requestFocus();
Also it seems like you want to pass key events to your Applet rather than your Canvas from your question.
In this case, try this in your AppletDemo constructor:
this.setFocusable(true);
this.requestFocus();
After that, you should receive keyboard input by default to the component that is focused.
Upvotes: 0
Reputation: 3830
Since what you really want is to create the applet as a child window, the easy solution would be to convince the applet to be your children, not forcefully adopting it, and working against both Windows and the JVM.
Luckily, the Sun/Oracle Java VM comes with a class called WComponentFrame
(Windows-only as implied from the name). It can constructed from an hwnd
, which you can send from your parent process. The applet can then be added as a child of your window.
import sun.awt.windows.WComponentPeer;
frame = new WEmbeddedFrame(hwnd);
frame.setLayout(new BorderLayout());
frame.add(applet, BorderLayout.CENTER);
Upvotes: 1