Skip to content

Instantly share code, notes, and snippets.

@seraphy
Last active December 31, 2018 04:11
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save seraphy/f428874e84a2378293a9a889d2d5f388 to your computer and use it in GitHub Desktop.
Save seraphy/f428874e84a2378293a9a889d2d5f388 to your computer and use it in GitHub Desktop.
SwingのJava9以降で、Hi-DPI環境下で暗黙で適用されるスケールを明示的に解除する実験例。 および、スケールされている環境下でロードしたイメージの実ピクセルサイズでの描画を行う実験例。およびスケール環境下でのマウス座標の確認。
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
package jp.seraphyware.java10learn;
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.Image;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.font.LineMetrics;
import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.Base64;
import java.util.Base64.Decoder;
import javax.imageio.ImageIO;
import javax.swing.AbstractAction;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
/**
* SwingのJava9以降で、Hi-DPI環境下では自動的にスケールがかかるようになっている。
* このスケールを明示的に解除して描画させるための実験。
*
* スケールはGraphis2Dに既定でかかっているので、これを明示的に解除すればよい。
* ただし、メニューバーのオフセットなど、transformがかかっている可能性があるので解除は注意のこと。
*
* スケールを解除せず、画像だけスケールさせたくない場合は、、
* 画像サイズをスケールで割った小さなサイズを指定すればスケールで相殺されて、実ピクセルサイズで描画されることになる。
*
* https://stackoverflow.com/questions/43057457/jdk-9-high-dpi-disable-for-specific-panel
* https://bugs.openjdk.java.net/browse/JDK-8189416
* https://superuser.com/questions/988379/how-do-i-run-java-apps-upscaled-on-a-high-dpi-display
*
* System Propertyで、
* -Dsun.java2d.uiScale=2.5
* のように明示的にスケールを指定して実験することができる。
*/
public class SwingScaleExample extends JFrame {
public SwingScaleExample() {
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
onClosing();
}
});
try {
initLayout();
} catch (Exception ex) {
dispose();
throw ex;
}
}
protected void onClosing() {
dispose();
}
private MyPanel myPanel = new MyPanel();
private void initLayout() {
setTitle(getClass().getSimpleName());
setSize(400, 200);
Container container = getContentPane();
container.setLayout(new BorderLayout());
container.add(myPanel, BorderLayout.CENTER);
JMenuBar menubar = new JMenuBar();
JMenu menuFile = new JMenu("File");
JMenuItem menuFileClose = new JMenuItem(new AbstractAction("Close") {
@Override
public void actionPerformed(ActionEvent e) {
onClosing();
}
});
menuFile.add(menuFileClose);
menubar.add(menuFile);
JMenu menuView = new JMenu("View");
JCheckBoxMenuItem menuViewScaleOff = new JCheckBoxMenuItem(new AbstractAction("Scale Off") {
@Override
public void actionPerformed(ActionEvent e) {
JCheckBoxMenuItem menuItem = (JCheckBoxMenuItem) e.getSource();
myPanel.setScaleOff(menuItem.isSelected());
}
});
menuView.add(menuViewScaleOff);
menubar.add(menuView);
setJMenuBar(menubar);
Image img = readImageFromBase64("box.png.asc");
System.out.println("img=" + img);
myPanel.setImage(img);
JLabel status = new JLabel();
container.add(status, BorderLayout.NORTH);
// スケールされた環境でのマウス座標のとりかた
myPanel.addMouseMotionListener(new MouseMotionListener() {
private int marker;
@Override
public void mouseMoved(MouseEvent e) {
print("move", e);
}
@Override
public void mouseDragged(MouseEvent e) {
print("drag", e);
}
private void print(String prefix, MouseEvent e) {
// デバイスのデフォルトのスケールを取得する
GraphicsConfiguration gconf = getGraphicsConfiguration();
AffineTransform at = gconf.getDefaultTransform();
double scalex = at.getScaleX();
double scaley = at.getScaleY();
// マウス座標
Point pt = e.getPoint();
double x = pt.getX();
double y = pt.getX();
// デバイス上の実ピクセル位置
int real_x = (int) (x * scalex);
int real_y = (int) (x * scaley);
// 通知回数をみるためのカウンタ
marker++;
status.setText((marker % 100) + " " + prefix + " x=" + x + ", y=" + y + ", scalex=" + scalex
+ ", real_x=" + real_x + ", real_y=" + real_y);
}
});
}
/**
* base64で格納されているリソースからイメージを構築する
* (gistにアップロードする都合上、バイナリを避けているだけ)
* @param name
* @return
*/
private BufferedImage readImageFromBase64(String name) {
BufferedImage img = null;
try (InputStream is = getClass().getResourceAsStream(name)) {
if (is != null) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buf = new byte[4096];
for (;;) {
int rd = is.read(buf);
if (rd < 0) {
break;
}
bos.write(buf, 0, rd);
}
Decoder decoder = Base64.getMimeDecoder();
byte[] data = decoder.decode(bos.toByteArray());
try (InputStream is2 = new ByteArrayInputStream(data)) {
img = ImageIO.read(is2);
}
}
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
return img;
}
public static void main(String[] args) throws Exception {
// システムデフォルトのL&Fに設定
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
SwingUtilities.invokeLater(() -> {
SwingScaleExample inst = new SwingScaleExample();
inst.setLocationByPlatform(true);
inst.setVisible(true);
});
}
}
/**
* 画像表示パネル
*/
class MyPanel extends JPanel {
/**
* スケール解除フラグ
*/
private boolean scaleOff;
/**
* イメージ
*/
private Image image;
public void setImage(Image image) {
this.image = image;
repaint();
}
public Image getImage() {
return image;
}
public void setScaleOff(boolean scaleOff) {
this.scaleOff = scaleOff;
repaint();
}
public boolean isScaleOff() {
return scaleOff;
}
@Override
protected void paintComponent(Graphics g0) {
Graphics2D g = (Graphics2D) g0;
// サブピクセルのレンダリング (このヒントがあるとなめらかになる)
// https://stackoverflow.com/questions/12415640/is-there-any-way-to-draw-lines-with-subpixel-precision-in-swing
g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
super.paintComponent(g);
AffineTransform at = g.getTransform();
double scalex = at.getScaleX();
double scaley = at.getScaleY();
System.out.print("at=" + at);
int w = getWidth();
int h = getHeight();
int real_w = (int) (w * scalex);
int real_h = (int) (h * scaley);
String msg = "virtual w=" + w + ",h=" + h + ", scale x=" + scalex + ", y=" + scaley + ", real w=" + real_w
+ ",h=" + real_h;
System.out.println(msg);
Font font = g.getFont();
FontMetrics fm = g.getFontMetrics();
LineMetrics lm = fm.getLineMetrics(msg, g);
float fontHeight = lm.getHeight();
int dw, dh;
if (scaleOff) {
// 暗黙で適用されているスケールを解除する。
// ※ メニューバー分のオフセットがかかっているので
// スケール解除時にオフセットは残すようにする
double offsetx = at.getTranslateX();
double offsety = at.getTranslateY();
at.setToTranslation(offsetx, offsety);
//at.scale(1, 1);
g.setTransform(at);
// スケールが解除されるので描画領域を実サイズとする
dw = real_w;
dh = real_h;
} else {
// スケールされた状態の描画範囲
dw = w;
dh = h;
}
// 論理10pxごとの矩形と、情報の表示
Color oldColor = g.getColor();
g.setColor(Color.RED);
for (int x = 0; x < dw; x += 10) {
g.drawLine(x, 0, x, dh);
}
for (int y = 0; y < dh; y += 10) {
g.drawLine(0, y, dw, y);
}
// スケール環境でのサブピクセルの描画
// (scaleがx2の場合に10pxの範囲に10本の線が見えていればサブピクセルの描画成功)
g.setColor(Color.LIGHT_GRAY);
g.setStroke(new BasicStroke((float) (1 / scalex)));
for (double x = 0; x < 10; x += 2 / scalex) { // 1論理ドットおきのラインをスケールで割った単位
g.draw(new Line2D.Double(x, 0, x, dh));
}
for (double y = 0; y < 10; y += 2 / scaley) { // 1論理ドットおきのラインをスケールで割った単位
g.draw(new Line2D.Double(0, y, dw, y));
}
g.setColor(Color.BLACK);
for (int x = 0; x < dw; x += 10) {
if (x % 3 == 0) {
g.drawString(Integer.toString(x), x + 5, fontHeight);
}
}
for (int y = 0; y < dh; y += 10) {
if (y % 3 == 0) {
g.drawString(Integer.toString(y), 5, y + fontHeight);
}
}
g.setColor(Color.BLUE);
g.drawString(msg, 30, fontHeight * 2);
String msg2 = "font size=" + font.getSize2D() + ", height=" + fontHeight;
g.drawString(msg2, 30, fontHeight * 3);
g.setColor(oldColor);
// 画像(ラスター)のスケール調整の実験
if (image != null) {
int img_real_w = image.getWidth(null);
int img_real_h = image.getHeight(null);
System.out.println("image imgw=" + img_real_w + "px, imgh=" + img_real_h + "px");
//g.drawImage(image, 50, 50, null); // 通常
g.drawImage(image, 50, 50, img_real_w, img_real_h, null); // 通常指定。幅、高さもスケールされる
int draw_w, draw_h;
if (!scaleOff) {
// スケールがかかっている場合の画像表示を、スケール解除して描画するためには、
// 画像サイズをスケールで割って、倍率のかかっていない場合の画像サイズを求める。
draw_w = (int) (img_real_w / scalex);
draw_h = (int) (img_real_h / scaley);
} else {
// スケール解除されているので、実サイズそのままで良い
draw_w = img_real_w;
draw_h = img_real_h;
}
g.drawImage(image, 50, 150, draw_w, draw_h, null); // 画像の範囲を明示的にスケール補正して指定
//g.drawImage(image, 50, 150, 50 + draw_w, 150 + draw_h, 0, 0, img_real_w, img_real_h, null);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment