Rendering sfocato di SwingNode in JavaFX su Windows
Panoramica
Utilizzo di FlyingSaucer all'interno di un'applicazione JavaFX, per evitare WebView per vari motivi:
- non fornisce accesso API diretto alle sue barre di scorrimento per un comportamento sincrono;
- raggruppa JavaScript, che è un grosso problema per il mio caso d'uso; e
- non è stato eseguito su Windows.
FlyingSaucer utilizza Swing, che richiede il wrapping XHTMLPanel
(una sottoclasse di JPanel
) in a SwingNode
da utilizzare insieme a JavaFX. Tutto funziona alla grande, l'applicazione esegue il rendering di Markdown in tempo reale ed è reattivo. Ecco un video dimostrativo dell'applicazione in esecuzione su Linux.
Problema
Il rendering del testo su Windows è sfocato. Quando si esegue in a JFrame
, non avvolto da a SwingNode
, ma ancora parte della stessa applicazione mostrata nel video, la qualità del testo è impeccabile. La cattura dello schermo mostra la finestra principale dell'applicazione (in basso), che include SwingNode
il suddetto JFrame
(in alto). Potrebbe essere necessario ingrandire il bordo diritto della "l" o "k" per vedere perché uno è nitido e l'altro sfocato:

Questo accade solo su Windows. Quando si visualizza il carattere su Windows tramite il programma di anteprima dei caratteri del sistema, i caratteri sono antialiasing utilizzando i colori LCD. L'applicazione utilizza la scala di grigi. Sospetto che se esiste un modo per forzare il rendering a utilizzare il colore per l'antialiasing anziché la scala di grigi, il problema potrebbe scomparire. Poi di nuovo, quando si esegue all'interno del proprio JFrame
, non ci sono problemi ei colori LCD non vengono utilizzati.
Codice
Ecco il codice per JFrame
che ha un rendering perfetto:
private static class Flawless {
private final XHTMLPanel panel = new XHTMLPanel();
private final JFrame frame = new JFrame( "Single Page Demo" );
private Flawless() {
frame.getContentPane().add( new JScrollPane( panel ) );
frame.pack();
frame.setSize( 1024, 768 );
}
private void update( final org.w3c.dom.Document html ) {
frame.setVisible( true );
try {
panel.setDocument( html );
} catch( Exception ignored ) {
}
}
}
Il codice per lo sfocato SwingNode
è un po 'più complicato (vedi l'elenco completo ), ma qui ci sono alcuni frammenti rilevanti (nota che si HTMLPanel
estende da XHTMLPanel
solo per sopprimere alcuni scorrimenti automatici indesiderati durante gli aggiornamenti):
private final HTMLPanel mHtmlRenderer = new HTMLPanel();
private final SwingNode mSwingNode = new SwingNode();
private final JScrollPane mScrollPane = new JScrollPane( mHtmlRenderer );
// ...
final var context = getSharedContext();
final var textRenderer = context.getTextRenderer();
textRenderer.setSmoothingThreshold( 0 );
mSwingNode.setContent( mScrollPane );
// ...
// The "preview pane" contains the SwingNode.
final SplitPane splitPane = new SplitPane(
getDefinitionPane().getNode(),
getFileEditorPane().getNode(),
getPreviewPane().getNode() );
Esempio di lavoro minimo
Ecco un esempio autonomo abbastanza minimale:
import javafx.application.Application;
import javafx.application.Platform;
import javafx.embed.swing.SwingNode;
import javafx.scene.Scene;
import javafx.scene.control.SplitPane;
import javafx.stage.Stage;
import org.jsoup.Jsoup;
import org.jsoup.helper.W3CDom;
import org.xhtmlrenderer.simple.XHTMLPanel;
import javax.swing.*;
import static javax.swing.SwingUtilities.invokeLater;
import static javax.swing.UIManager.getSystemLookAndFeelClassName;
import static javax.swing.UIManager.setLookAndFeel;
public class FlyingSourceTest extends Application {
private final static String HTML = "<!DOCTYPE html><html><head" +
"><style type='text/css'>body{font-family:serif; background-color: " +
"#fff; color:#454545;}</style></head><body><p style=\"font-size: " +
"300px\">TEST</p></body></html>";
public static void main( String[] args ) {
Application.launch( args );
}
@Override
public void start( Stage primaryStage ) {
invokeLater( () -> {
try {
setLookAndFeel( getSystemLookAndFeelClassName() );
} catch( Exception ignored ) {
}
primaryStage.setTitle( "Hello World!" );
final var renderer = new XHTMLPanel();
renderer.getSharedContext().getTextRenderer().setSmoothingThreshold( 0 );
renderer.setDocument( new W3CDom().fromJsoup( Jsoup.parse( HTML ) ) );
final var swingNode = new SwingNode();
swingNode.setContent( new JScrollPane( renderer ) );
final var root = new SplitPane( swingNode, swingNode );
// ----------
// Here be dragons? Using a StackPane, instead of a SplitPane, works.
// ----------
//StackPane root = new StackPane();
//root.getChildren().add( mSwingNode );
Platform.runLater( () -> {
primaryStage.setScene( new Scene( root, 300, 250 ) );
primaryStage.show();
} );
} );
}
}
Cattura sfocata dall'esempio di lavoro minimo; lo zoom in avanti rivela che i bordi delle lettere sono fortemente antialias piuttosto che netti contrasti:

L'uso di a JLabel
mostra anche lo stesso rendering fuzzy:
final var label = new JLabel( "TEST" );
label.setFont( label.getFont().deriveFont( Font.BOLD, 128f ) );
final var swingNode = new SwingNode();
swingNode.setContent( label );
Tentativi
Ecco la maggior parte dei modi in cui ho provato a rimuovere la sfocatura.
Giava
Sul lato Java, qualcuno ha suggerito di eseguire l'applicazione utilizzando:
-Dawt.useSystemAAFontSettings=off
-Dswing.aatext=false
Nessuno dei suggerimenti per il rendering del testo ha aiutato.
Impostazione del contenuto del SwingNode
raggio SwingUtilities.invokeLater
non ha alcun effetto.
JavaFX
Qualcun altro ha detto che la disattivazione della cache ha aiutato, ma era per un JavaFX ScrollPane
, non per uno all'interno di un file SwingNode
. Non ha funzionato.
Il JScrollPane
contenuto da SwingNode
ha il suo allineamento X e allineamento Y impostati rispettivamente a 0,5 e 0,5. Si consiglia di garantire un offset di mezzo pixel altrove . Non riesco a immaginare che l'impostazione Scene
da utilizzare StrokeType.INSIDE
possa fare alcuna differenza, anche se ho provato a utilizzare una larghezza del tratto di 1 senza alcun risultato.
FlyingSaucer
FlyingSaucer ha una serie di opzioni di configurazione . Varie combinazioni di impostazioni includono:
java -Dxr.text.fractional-font-metrics=true \
-Dxr.text.aa-smoothing-level=0 \
-Dxr.image.render-quality=java.awt.RenderingHints.VALUE_INTERPOLATION_BICUBIC
-Dxr.image.scale=HIGH \
-Dxr.text.aa-rendering-hint=VALUE_TEXT_ANTIALIAS_GASP -jar ...
Le xr.image.
impostazioni influenzano solo le immagini renderizzate da FlyingSaucer, piuttosto che il modo in cui l'output di FlyingSaucer viene reso da JavaFX all'interno del file SwingNode
.
Il CSS utilizza i punti per le dimensioni dei caratteri.
Ricerca
- https://stackoverflow.com/a/26227562/59087 - sembra che alcune soluzioni possano essere utili.
- https://bugs.openjdk.java.net/browse/JDK-8089499- non sembra essere applicabile perché utilizza
SwingNode
eJScrollPane
. - https://stackoverflow.com/a/24124020/59087 - probabilmente non è rilevante perché non è in uso un generatore di scene XML.
- https://www.cs.mcgill.ca/media/tech_reports/42_Lessons_Learned_in_Migrating_from_Swing_to_JavaFX_LzXl9Xv.pdf - la pagina 8 descrive lo spostamento di 0,5 pixel, ma come?
- https://dlsc.com/2014/07/17/javafx-tip-9-do-not-mix-swing-javafx/ - suggerisce di non mescolare JavaFX e Swing, ma il passaggio a Swing puro non è un'opzione: riscriverei prima l'app in un'altra lingua.
Accettato come bug contro OpenJDK / JavaFX:
- https://bugs.openjdk.java.net/browse/JDK-8252255
JDK e JRE
Utilizzo di OpenJDK di Bellsoft con JavaFX in bundle. Per quanto ne so, OpenJDK ha il supporto Freetype da un po 'di tempo. Inoltre, il carattere ha un bell'aspetto su Linux, quindi probabilmente non è JDK.
Schermo
Le seguenti specifiche dello schermo mostrano il problema, ma altre persone (che guardano su diversi monitor e risoluzioni, senza dubbio) hanno menzionato il problema.
- 15,6 "4: 3 HD (1366 x 768)
- Full HD (1920x1080)
- Retroilluminazione a LED ad ampio angolo di visione
- ASUS n56v
Domanda
Perché FlyingSaucer XHTMLPanel
quando avvolto all'interno SwingNode
diventa sfocato su Windows, eppure la visualizzazione dello stesso XHTMLPanel
in JFrame
esecuzione nella stessa applicazione JavaFX appare nitida? Come si risolve il problema?
Il problema coinvolge SplitPane
.
Risposte
Ci sono alcune opzioni che potresti provare anche se devo ammettere che non conosco FlyingSaucer e la sua API.
FlyingSaucer ha diversi renderer. Quindi potrebbe essere possibile evitare completamente il rendering Swing / AWT utilizzando invece questa libreria per eseguire tutto il rendering direttamente in JavaFX.https://github.com/jfree/fxgraphics2d
Un'altra possibilità è consentire a FlyingSaucer di eseguire il rendering in un'immagine che può essere visualizzata in JavaFX in modo molto efficiente tramite buffer diretti. Vedi il codice AWTImage nel mio repository qui:https://github.com/mipastgt/JFXToolsAndDemos
Non sono stato in grado di riprodurre il problema da solo, quindi potrebbe esserci qualche problema nella combinazione della versione JDK / JavaFX che stai utilizzando. È anche possibile che il problema si verifichi solo con una combinazione specifica di dimensioni del display e ridimensionamento dello schermo.
La mia configurazione è la seguente:
- JavaFX 14
- OpenJDK 14
import javafx.application.Application;
import javafx.application.Platform;
import javafx.embed.swing.SwingNode;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import org.jsoup.Jsoup;
import org.jsoup.helper.W3CDom;
import org.jsoup.nodes.Document;
import org.xhtmlrenderer.simple.XHTMLPanel;
import javax.swing.*;
public class FlyingSourceTest extends Application {
private final static String HTML_PREFIX = "<!DOCTYPE html>\n"
+ "<html>\n"
+ "<body>\n";
private static final String HTML_CONTENT =
"<p style=\"font-size:500px\">TEST</p>";
private final static String HTML_SUFFIX = "<p style='height=2em'> </p></body></html>";
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage primaryStage) {
SwingUtilities.invokeLater(() -> {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException e) {
e.printStackTrace();
}
primaryStage.setTitle("Hello World!");
XHTMLPanel mHtmlRenderer = new XHTMLPanel();
mHtmlRenderer.getSharedContext().getTextRenderer().setSmoothingThreshold(0);
SwingNode mSwingNode = new SwingNode();
JScrollPane mScrollPane = new JScrollPane(mHtmlRenderer);
String htmlContent = HTML_PREFIX + HTML_CONTENT + HTML_SUFFIX;
Document jsoupDoc = Jsoup.parse(htmlContent);
org.w3c.dom.Document w3cDoc = new W3CDom().fromJsoup(jsoupDoc);
mHtmlRenderer.setDocument(w3cDoc);
mSwingNode.setContent(mScrollPane);
// AnchorPane anchorPane = new AnchorPane();
// anchorPane.getChildren().add(mSwingNode);
// AnchorPane.setTopAnchor(mSwingNode, 0.5);
// AnchorPane.setLeftAnchor(mSwingNode, 0.5);
// mSwingNode.setTranslateX(0.5);
// mSwingNode.setTranslateY(0.5);
StackPane root = new StackPane();
root.getChildren().add(mSwingNode);
Platform.runLater(() -> {
primaryStage.setScene(new Scene(root, 300, 250));
primaryStage.show();
});
});
}
}
Il problema è stato accettato come bug contro OpenJDK / JavaFX:
- https://bugs.openjdk.java.net/browse/JDK-8252255
Nessuno dei suggerimenti di Mipa funzionerebbe nella pratica. FlyingSaucer è strettamente integrato con un JScrollPane
, che preclude la possibilità di forzare FlyingSaucer a eseguire il rendering su un pannello basato su JavaFX.
Un'altra possibilità è andare nella direzione opposta: creare un'applicazione Swing e incorporare controlli JavaFX, ad esempio utilizzando un JFXPanel ; tuttavia, sembrerebbe più prudente accettare il comportamento sfocato fino a quando il bug non viene eliminato.