JavaFX のIME候補ポップアップ表示位置不正のワークアラウンド

この記事では IME関連のバグの一つ[JDK-8189282] の回避方法を紹介します。

ここで紹介するコードは一時的な解決です。将来の JavaFX のバージョンでは動作しないかもしれないので注意してください。 また OpenJFX でこのバグが修正された際には、この回避コードは除外する必要があります。

問題

「JavaFX: Invalid position of candidate pop-up of InputMethod in Hi-DPI on Windows」 https://bugs.openjdk.java.net/browse/JDK-8189282

この問題は私が2017年に報告していたもので、報告後に別のより良いワークアラウンドを見つけました。 IM (Input method)を使用する言語圏では、JavaFX 技術の採否を分けるレベルの重要度があると考えられます。

環境

この問題は次の環境で発生しています。

  • Windows 10

  • HIDPI

  • JDK 8u60, 9, 10

8u60以降では、スケールが150% 以上になる場合に、HIDPI モードがデフォルトで有効になることで、 この問題は顕在化します。

解決

私はこの問題では2つの回避方法を見つけています。

消極的な解決

最初に思いつく方法は HIDPI モードをオフにする方法で簡単ですが、 進歩的ではありません。

JVM の起動パラメタ
-Dprism.allowhidpi=false

積極的な解決

バグの報告後に見つけた回避方法を説明します。

放置されていますが、問題の回避はそれほど難しくはありません。

テキスト入力をサポートするコントロールでは、 InputMethodRequests インタフェースでIME の表示位置を制御できますので、 正しいスケールで補正することで問題を回避できます。

if(SystemUtils.IS_OS_WINDOWS) {
final InputMethodRequests imr = textControl.getInputMethodRequests();
textControl.setInputMethodRequests(new WindowsInputMethodRequests(imr));
}
WindowsInputMethodRequests.java
/**
* @author Naohiro Yoshimoto (yosbits.com)
*/

public final class WindowsInputMethodRequests implements ExtendedInputMethodRequests {

private final InputMethodRequests imr;

public WindowsInputMethodRequests(final InputMethodRequests imr) {
this.imr = imr;
}

private static double getUIScale() {
try {
Method method=null;
final Screen mainScreen = Screen.getMainScreen();
final Class<? extends Screen> mainScreenClass = mainScreen.getClass();
try {
method = mainScreenClass.getDeclaredMethod("getPlatformScaleX");
} catch (NoSuchMethodException e) {
try {
method = mainScreenClass.getDeclaredMethod("getUIScale");
} catch (NoSuchMethodException e1) {
//do nothing
}
}
if(method==null) {
return 1;
}
return ((Number)method.invoke(mainScreen)).doubleValue();
}catch(Exception e) {
e.printStackTrace();

}

return 1;
}

@Override
public Point2D getTextLocation(int offset) {
double scale = getUIScale();
return imr.getTextLocation(offset).multiply(scale);
}

@Override
public int getLocationOffset(int x, int y) {
double scale = getUIScale();
return (int)(imr.getLocationOffset(x,y)*scale);
}

@Override
public void cancelLatestCommittedText() {
imr.cancelLatestCommittedText();
}

@Override
public String getSelectedText() {
System.out.println(imr.getSelectedText());

return imr.getSelectedText();
}

@Override
public int getInsertPositionOffset() {
double scale = getUIScale();
return (int)(((ExtendedInputMethodRequests)imr).getInsertPositionOffset() * scale);
}

//TODO: It has not been confirmed whether this method is used.
@Override
public String getCommittedText(int begin, int end) {
System.out.println("getCommittedText");
String text = ((ExtendedInputMethodRequests)imr).getCommittedText(begin, end);
System.out.println(text);
return text;
}

//TODO: It has not been confirmed whether this method is used.
@Override
public int getCommittedTextLength() {
System.out.println("getCommittedTextLength");
return ((ExtendedInputMethodRequests)imr).getCommittedTextLength();
}
}

このワークアラウンドはJDK 8, 9以降での動作を確認しています。 このコードを LnF(Skin) で実装すれば、この回避方法を意識する必要はなくなります。

YOSBITS では JavaFX のコンサルティングを行なっていますので、 他に質問があればメールで承ります。 コンタクトを参照してください。