【发布时间】:2015-07-27 06:47:33
【问题描述】:
我有一个应用程序可以抓取 X 窗口并将它们作为子窗口托管。 它以某种方式实现了一个非常基本的窗口管理器。没有窗口管理器正在运行。 除 Java 应用程序外,运行良好。
例如,如果 Java 应用程序有菜单或弹出窗口,则这些弹出窗口没有正确重新定位。 我用一个非常简单的单元测试重现了这一点。 Java 应用程序是一个基本的 JFrame,带有一个菜单栏和一个菜单
import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JMenuBar;
public class test extends JFrame {
private static test window;
public test() {
initialize();
}
/**
* @param args
*/
public static void main(String[] args) {
System.out.println("Hello world");
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
window = new test();
window.setVisible(true);
}
});
}
/**
* Initialize the contents of the frame.
*/
private void initialize() {
setTitle("ResizeTest");
setBounds(100, 100, 888, 439);
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
getContentPane().setLayout(new BorderLayout(0, 0));
JMenuBar menuBar = new JMenuBar();
getContentPane().add(menuBar, BorderLayout.NORTH);
JMenu fileMenu = new JMenu("File");
fileMenu.add("Open");
fileMenu.add("Close");
menuBar.add(fileMenu);
add(menuBar);
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
dispose();
System.exit(0);
}
});
JPanel panel = new JPanel();
panel.add(new JLabel("Hello world"));
getContentPane().add(panel, BorderLayout.SOUTH);
pack();
}
}
然后是另一个单元测试(在 C 中):
/*
Simple Xlib application drawing a box in a window.
To Compile: gcc -O2 -Wall -o test test.c -L /usr/X11R6/lib -lX11 -lm
*/
#include<X11/Xlib.h>
#include<stdio.h>
#include<stdlib.h> // prevents error for exit on line 18 when compiling with gcc
#include <X11/X.h>
#include <X11/Xatom.h>
#include <X11/Intrinsic.h>
#include <X11/StringDefs.h>
#include <X11/Shell.h>
int WINDOW_ID;
void processExistingWindows(Display *display, Window parentWindow) {
Window *children;
Window parent;
Window root;
unsigned int nchildren;
printf("Entering processExistingWindows\n");
Window rootWindow = XDefaultRootWindow(display);
int result = XQueryTree(display, rootWindow, &root, &parent, &children, &nchildren);
printf("XQueryTree result is %d\n", result);
unsigned int windowCount = 0;
printf("Iterating through %d windows\n", nchildren);
for (windowCount = 0; result && windowCount < nchildren; windowCount++) {
Window currentWindow = children[windowCount];
if ((int)currentWindow == WINDOW_ID) {
int reparentResult = XReparentWindow(display, currentWindow, parentWindow, 0, 0);
printf("XReparentWindow result: %d\n", reparentResult);
printf("Reparented window: %x into %x\n", (int)currentWindow, (int)parentWindow);
}
}
if (result && children != NULL) {
XFree((char *) children);
}
}
int main(int argc, char** argv) {
Display *d;
int s;
Window w;
XEvent e;
if (argc < 2) {
printf("Please give the window ID you want to reparent\n");
exit(1);
}
WINDOW_ID = atoi(argv[1]);
printf("Will try to reparent the Window: %x \n", WINDOW_ID);
printf("Forcing synchronous calls to the X Server\n");
_Xdebug = 1; // To allow proper debugging by forcing synchronous calls to the X Server
/* open connection with the server */
d=XOpenDisplay(NULL);
if(d==NULL) {
printf("Cannot open display\n");
exit(1);
}
s=DefaultScreen(d);
/* create window */
w = XCreateSimpleWindow(d, RootWindow(d, s), 0, 0, 400, 400, 1,
BlackPixel(d, s), WhitePixel(d, s));
printf("Current window ID is: %x\n", (int) w);
// Process Window Close Event through event handler so XNextEvent does Not fail
Atom delWindow = XInternAtom( d, "WM_DELETE_WINDOW", 0 );
XSetWMProtocols(d , w, &delWindow, 1);
/* select kind of events we are interested in */
XSelectInput(d, w, ExposureMask | KeyPressMask | StructureNotifyMask );
/* map (show) the window */
XMapWindow(d, w);
processExistingWindows(d, w);
/* event loop */
printf("Starting the loop\n");
XMoveWindow(d, w, 100, 200);
while(1) {
XNextEvent(d, &e);
/* draw or redraw the window */
if(e.type==Expose) {
XFillRectangle(d, w, DefaultGC(d, s), 20, 20, 10, 10);
}
/* exit on key press */
if(e.type==KeyPress)
break;
// Handle Windows Close Event
if(e.type==ClientMessage)
break;
}
/* destroy our window */
XDestroyWindow(d, w);
/* close connection to server */
XCloseDisplay(d);
return 0;
}
只需启动 Java 应用程序。 它显示正确,菜单显示在预期的位置。
获取窗口 ID(使用xwininfo -root -tree -int)并使用此窗口 ID 作为参数启动单元测试,以在基本窗口中重新设置它的父级。
Java 框架已正确移动并重新设置为窗口的父级,但菜单仍会在先前的 JFrame 位置弹出!
这就像 JFrame 被正确地重新定位,它的原点相对于它的新父窗口,但是菜单/弹出窗口没有考虑这个新位置。
这很奇怪。 如果我不执行 XReparentWindow 而不是执行 XMoveWindow,则菜单会出现在应有的位置。
我用谷歌搜索了这个问题,但没有任何明确的解决方案。 有很多关于 JVM 中平铺窗口管理器的硬编码支持的讨论,这显然是由 OpenJDK 解决的。但是我使用的版本都没有(Java 6、7、8 和 OpenJDK 7)。
虽然在我看来它就像 JDK 中的一个普通错误(我有其他基于 C/C++ 的应用程序以这种方式托管,可以很好地处理菜单/弹出窗口),但它使用窗口管理器(如 FVWM)工作的事实证明它可以工作。 我还查看了twm 中的实现,看起来并没有什么不同。
有没有人遇到(并解决过)这个问题?
谢谢!
编辑
经过数小时的调试,它确实来自 Java 以及它如何处理窗口管理器(或没有)。代码的有趣部分在 XDecoratedPeer.java 和 XWM.java 中。
Java 尝试猜测正在运行的窗口管理器,并将根据该窗口管理器调整其行为(它们并非都以相同的方式对待客户端应用程序)。在我们的例子中,没有运行窗口管理器,因此 Java 将默认为 No/Other WM。
一旦启动,Java 应用程序将永远不会重新检查窗口管理器的更改。因此,我们不能从主机内部“伪造”任何特定的 WM 来改变客人的行为,因为为时已晚。
在没有 WM 的情况下,Java 仍会对其接收到的各种 X 事件(单击、移动、重新设置 ..)做出反应,但预计会有非常严格的事件连续性。
在我们的例子中,ReparentNotifyEvent 不足以触发对 JVM 中窗口位置的完全重新计算。所以在从主人那里重新养育之后,客人仍然认为它在原来的位置。这不是立即可见的,因为所有窗口都是由 X 服务器相对于彼此绘制的,但是所有弹出窗口(以及由 Java 直接创建的所有窗口,即设置了 override_redirect 标志)将是在错误的位置绘制。
Java 需要重新计算其屏幕坐标的是 ConfigureNotifyEvent(其中包含绝对屏幕坐标)。
但是,此事件必须具有两个特征: - 不是合成的(即通过 XSendEvent() 调用显式发送) - 有一个不同于 ReparentNotify 事件的序列。这是棘手的一点,我不知道该怎么做,除了在 XReparentWindow 之间插入 sleep(1) 和 XSendEvent。 XFlush()、XSync()、XNoOp() 等 .. 不起作用,因为它们不会创建新请求,因此不会增加序列号。
我看过的所有 Java 版本都有几乎相同的行为。 无论如何 .. 进行此更改后,Java 客户端会正确地重新计算其屏幕位置,并且现在可以正确显示弹出窗口。
【问题讨论】:
-
为什么不直接运行一个真正的窗口管理器?
-
不幸的是,这不是一个选择......