├── .github
└── workflows
│ └── mavenpublish.yml
├── LICENSE
├── README.md
├── comparasion.png
├── pom.xml
├── src
└── main
│ └── java
│ └── ru
│ └── krlvm
│ └── swingdpi
│ ├── CustomBootstrap.java
│ ├── ScalableJFrame.java
│ └── SwingDPI.java
└── ui_defaults_list.txt
/.github/workflows/mavenpublish.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a package using Maven and then publish it to GitHub packages when a release is created
2 | # For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#apache-maven-with-a-settings-path
3 |
4 | name: Maven Package
5 |
6 | on:
7 | release:
8 | types: [created]
9 |
10 | jobs:
11 | build:
12 |
13 | runs-on: ubuntu-latest
14 | permissions:
15 | contents: read
16 | packages: write
17 |
18 | steps:
19 | - uses: actions/checkout@v2
20 | - name: Set up JDK 11
21 | uses: actions/setup-java@v2
22 | with:
23 | java-version: '11'
24 | distribution: 'adopt'
25 | server-id: github # Value of the distributionManagement/repository/id field of the pom.xml
26 | settings-path: ${{ github.workspace }} # location for the settings.xml file
27 |
28 | - name: Build with Maven
29 | run: mvn -B package --file pom.xml
30 |
31 | - name: Publish to GitHub Packages Apache Maven
32 | run: mvn deploy -s $GITHUB_WORKSPACE/settings.xml
33 | env:
34 | GITHUB_TOKEN: ${{ github.token }}
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 krlvm
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SwingDPI
2 | Make using your Swing application convenient on HiDPI screens
3 |
4 | SwingDPI allows you to scale your application for convenient using on screens with high resolution (e.g. FullHD, 4K)
5 |
6 | You can download the latest build from `Releases` tab, supports Java 7 and later
7 |
8 | 
9 | ###### Swing application that running with DPI-scaling compared with the same application without it
10 |
11 | ### Using in a project
12 | At first, you need to connect SwingDPI to your project. You can download .jar from the releases section and connect it to your project as a dependency, which will be extracted into your jarfile. After that, you can import SwingDPI API to your program loader class: `import ru.krlvm.swingdpi.SwingDPI`
13 |
14 | You also can add SwingDPI using Maven:
15 | ```
16 |
14 | * API of SwingDPI 15 | *
16 | * SwingDPI allows you to scale your application for convenient using on HiDPI screens
17 | * Call SwingDPI.applyScalingAutomatically() on your application start for easy scaling
18 | * GitHub Page: https://github.com/krlvm/SwingDPI
19 | *
20 | * @author krlvm
21 | */
22 | public class SwingDPI {
23 |
24 | public static final String VERSION = "1.2";
25 |
26 | //is scale factor set
27 | private static boolean scaleFactorSet = false;
28 | //the applied scale factor, e.g. 1.25 when the system DPI scaling is 125%
29 | private static float scaleFactor = 1.0f;
30 | //default scale factor, 100% scale
31 | private static final float DEFAULT_SCALE_FACTOR = 1.0f;
32 | //is DPI scale applied
33 | private static boolean scaleApplied = false;
34 | //exclude/whitelist defaults (something one)
35 | private static Set
52 | * Java 9+ Native Scaling is very buggy and have a poor font rendering
53 | * If you don't want to disable the Java 9 Scaling, SwingDPI will not work
54 | * to avoid double scaling
55 | */
56 | public static void applyScalingAutomatically(boolean disableJava9NativeScaling) {
57 | if (isJava9) {
58 | if (!disableJava9NativeScaling) {
59 | return;
60 | } else {
61 | disableJava9NativeScaling();
62 | }
63 | }
64 | determineScaleFactor();
65 | if (scaleFactor != DEFAULT_SCALE_FACTOR) {
66 | setScaleApplied(true);
67 | }
68 | }
69 |
70 | /**
71 | * Determines, sets the system DPI scaling setting and retrieves scale factor
72 | * Returns 1.0 if Java 9 scaling is preferred
73 | *
74 | * @return DPI scale factor
75 | */
76 | public static float determineScaleFactor() {
77 | return isJava9 && !java9ScalingDisabled ? 1.0F : _determineScaleFactor();
78 | }
79 |
80 | /**
81 | * Determines, sets the system DPI scaling setting and retrieves scale factor
82 | *
83 | * @return DPI scale factor
84 | */
85 | public static float _determineScaleFactor() {
86 | float resolution = Toolkit.getDefaultToolkit().getScreenResolution(); //gets the screen resolution in percent, i.e. system DPI scaling
87 | if (resolution != 100.0f) { //when the system DPI scaling is not 100%
88 | setScaleFactor(resolution / 96.0f); //divide the system DPI scaling by default (100%) DPI and get the scale factor
89 | }
90 | return scaleFactor;
91 | }
92 |
93 | /**
94 | * Applies/disables scale for new and existing frames
95 | *
96 | * @param apply - enable or disable scaling
97 | */
98 | public static void setScaleApplied(boolean apply) {
99 | setScaleApplied(apply, true);
100 | }
101 |
102 | /**
103 | * Applies/disables scale for new and the existing frames
104 | *
105 | * @param apply - enable or disable scaling
106 | * @param scaleExistingFrames - enable or disable scaling for existing frames
107 | */
108 | public static void setScaleApplied(boolean apply, boolean scaleExistingFrames) {
109 | if (apply == scaleApplied) {
110 | return; // scale already applied/disabled
111 | }
112 | scaleApplied = apply;
113 | if (!apply) {
114 | setScaleFactor(1.0f); // after that, the scaling factor should be determined again
115 | }
116 |
117 | UIDefaults defaults = UIManager.getLookAndFeelDefaults(); // gets the Swing UI defaults - we will writing in them
118 | for (Object key : Collections.list(defaults.keys())) { // processing all default UI keys
119 | if(isWindowsLF() && Arrays.asList
120 | (
121 | "RadioButtonMenuItem.font",
122 | "CheckBoxMenuItem.font",
123 | "MenuBar.font",
124 | "PopupMenu.font",
125 | "MenuItem.font",
126 | "Menu.font",
127 | "ToolTip.font"
128 | ).contains(key.toString())) {
129 | continue;
130 | }
131 | if (BLACKLISTED_DEFAULTS != null) {
132 | if (BLACKLISTED_DEFAULTS.contains(key.toString())) {
133 | continue;
134 | }
135 | } else if (WHITELISTED_DEFAULTS != null) {
136 | if (!WHITELISTED_DEFAULTS.contains(key.toString())) {
137 | continue;
138 | }
139 | }
140 | Object original = defaults.get(key);
141 | Object newValue = scale(key, original);
142 | if (newValue != null && newValue != original) {
143 | defaults.put(key, newValue); //updating defaults
144 | }
145 | }
146 | fixJOptionPaneIcons();
147 | if (scaleExistingFrames) {
148 | for (Frame frame : Frame.getFrames()) { //gets all created frames
149 | if (!(frame instanceof JFrame)) {
150 | return;
151 | }
152 | Dimension dimension = frame.getSize();
153 | frame.setSize(scale(dimension));
154 | for (Component component : ((JFrame) frame).getContentPane().getComponents()) {
155 | dimension = component.getSize();
156 | Dimension newDimension = scale(dimension);
157 | if (component instanceof JTextField) {
158 | component.setPreferredSize(newDimension);
159 | } else {
160 | component.setSize(newDimension);
161 | }
162 | }
163 | }
164 | }
165 | }
166 |
167 | /**
168 | * Retrieves a boolean that determines whether scaling is applied or not.
169 | *
170 | * @return is scaling applied
171 | */
172 | public static boolean isScaleApplied() {
173 | return scaleApplied;
174 | }
175 |
176 | /**
177 | * Sets the scale factor
178 | *
179 | * @param scaleFactor - new scale factor
180 | */
181 | public static void setScaleFactor(float scaleFactor) {
182 | disableJava9NativeScaling(); // avoid double scaling
183 | if (!scaleFactorSet) {
184 | scaleFactorSet = true;
185 | }
186 | SwingDPI.scaleFactor = scaleFactor;
187 | }
188 |
189 | /**
190 | * Retrieves the current scale factor
191 | *
192 | * @return scale factor
193 | */
194 | public static float getScaleFactor() {
195 | if (!scaleFactorSet) {
196 | determineScaleFactor();
197 | }
198 | return scaleFactor;
199 | }
200 |
201 | /**
202 | * Retrieves a scaled version of the param from Swing UI defaults
203 | *
204 | * @param key - param key
205 | * @param original - original value
206 | * @param scaleFactor - scale factor
207 | * @return a scaled param version
208 | */
209 | private static Object scale(Object key, Object original, float scaleFactor) {
210 | if (original instanceof Font) {
211 | if (original instanceof FontUIResource && key.toString().endsWith(".font")) {
212 | int newSize = (int) (Math.round((float) ((Font) original).getSize()) * scaleFactor);
213 | return new FontUIResource(((Font) original).getName(), ((Font) original).getStyle(), newSize);
214 | }
215 | return original;
216 | }
217 | if (original instanceof Integer) {
218 | if (!endsWithOneOf((key instanceof String) ? ((String) key).toLowerCase() : "")) {
219 | return original;
220 | }
221 | return (int) ((Integer) original * scaleFactor);
222 | }
223 | return null;
224 | }
225 |
226 | /**
227 | * Retrieves a scaled version of the param from Swing UI defaults
228 | *
229 | * @param key - param key
230 | * @param original - original value
231 | * @return a scaled param version
232 | */
233 | private static Object scale(Object key, Object original) {
234 | return scale(key, original, scaleFactor);
235 | }
236 |
237 | /**
238 | * Scales dimension
239 | *
240 | * @param dimension - dimension to scale
241 | * @return a scaled version of the dimension
242 | */
243 | public static Dimension scale(Dimension dimension) {
244 | if (!scaleFactorSet) {
245 | return dimension;
246 | }
247 | dimension.setSize((int) (dimension.getWidth() * scaleFactor), (int) (dimension.getHeight() * scaleFactor));
248 | return dimension;
249 | }
250 |
251 | /**
252 | * Retrieves a scaled version of a dimension
253 | *
254 | * @param dimension - dimension to scale
255 | * @return a scaled version of the dimension
256 | */
257 | public static Dimension getScaledDimension(Dimension dimension) {
258 | if (!scaleFactorSet) {
259 | return dimension;
260 | }
261 | return new Dimension((int) (dimension.getWidth() * scaleFactor), (int) (dimension.getHeight() * scaleFactor));
262 | }
263 |
264 | public static Dimension scale(int width, int height) {
265 | return scale(new Dimension(width, height));
266 | }
267 |
268 | public static int scale(int i) {
269 | if (!scaleFactorSet) {
270 | return i;
271 | }
272 | return (int) (i * scaleFactor);
273 | }
274 |
275 | private static boolean endsWithOneOf(String text) {
276 | for (String suffix : new String[]{"width", "height", "indent", "size", "gap"}) {
277 | if (suffix.endsWith(text)) {
278 | return true;
279 | }
280 | }
281 | return false;
282 | }
283 |
284 | /**
285 | * Retrieves scaled version of dimension
286 | * The scaling algorithm is optimized for frame scaling
287 | *
288 | * @param dimension - dimension to scale
289 | * @return a scaled version of the dimension
290 | */
291 | public static Dimension scaleFrame(Dimension dimension) {
292 | if (!scaleFactorSet) {
293 | return dimension;
294 | }
295 | return scale((int) (dimension.width - (dimension.width * .2)), (int) (dimension.height - (dimension.height * .15)));
296 | }
297 |
298 | /**
299 | * If font of specific component did not scaled automatically use this method
300 | *
301 | * @param component - component for scale
302 | */
303 | public static void scaleFont(Component component) {
304 | setFontSize(component, component.getFont().getSize());
305 | }
306 |
307 | public static void setFontSize(Component component, float size) {
308 | Font font = component.getFont();
309 | component.setFont(font.deriveFont(size * scaleFactor));
310 | }
311 |
312 | // https://stackoverflow.com/questions/33926645/joptionpane-icon-gets-cropped-in-windows-10
313 | private static void fixJOptionPaneIcons() {
314 | if (!isWindowsLF() || isJava9 || (scaleFactor != 1.25 && scaleFactor != 1.5)) return;
315 | try {
316 | String[][] icons = {
317 | {"OptionPane.warningIcon", "65581"},
318 | {"OptionPane.questionIcon", "65583"},
319 | {"OptionPane.errorIcon", "65585"},
320 | {"OptionPane.informationIcon", "65587"}
321 | };
322 |
323 | //obtain a method for creating proper icons
324 | Method getIconBits = Class.forName("sun.awt.shell.Win32ShellFolder2").getDeclaredMethod("getIconBits", long.class, int.class);
325 | getIconBits.setAccessible(true);
326 | int icon32Size = (scaleFactor == 1) ? (32) : ((scaleFactor == 1.25) ? (40) : ((scaleFactor == 1.5) ? (45) : ((int) (32 * scaleFactor))));
327 | Object[] arguments = {null, icon32Size};
328 | for (String[] s : icons) {
329 | if (UIManager.get(s[0]) instanceof ImageIcon) {
330 | arguments[0] = Long.valueOf(s[1]);
331 | //this method is static, so the first argument can be null
332 | int[] iconBits = (int[]) getIconBits.invoke(null, arguments);
333 | if (iconBits != null) {
334 | //create an image from the obtained array
335 | BufferedImage img = new BufferedImage(icon32Size, icon32Size, BufferedImage.TYPE_INT_ARGB);
336 | img.setRGB(0, 0, icon32Size, icon32Size, iconBits, 0, icon32Size);
337 | ImageIcon newIcon = new ImageIcon(img);
338 | //override previous icon with the new one
339 | UIManager.put(s[0], newIcon);
340 | }
341 | }
342 | }
343 | } catch (Exception ex) {
344 | ex.printStackTrace();
345 | }
346 | }
347 |
348 | /**
349 | * Exclude some UI Defaults from scaling (blacklist)
350 | *
351 | * @param toExclude - UI Defaults to exclude
352 | * @throws IllegalStateException - if UI Defaults is in whitelist mode
353 | */
354 | public static void excludeDefaults(Collection