I need to do the following using Java SWT:
Display a list of Pixels as an Image
Allow the user to select a subset of Pixels
Display a Grid over the image as a guide for the user. The Image still needs to handle mouse events
1) and 2) are straightforward, however I don't know how to achieve 3).
Reading up on SWT, I do not see a way to put a Transparent Overlay over an image. Is this possible? Is there another method?
Just draw the grid on top of the Image. Set GG#setAlpha(int) to a low value to make the lines transparent. This will not interfere with mouse events:
public static void main(String[] args)
{
final Display display = new Display();
final Shell shell = new Shell(display);
shell.setText("Stackoverflow");
shell.setLayout(new FillLayout());
Image image = new Image(display, "baz.png");
Label label = new Label(shell, SWT.NONE);
label.addListener(SWT.Paint, new Listener()
{
#Override
public void handleEvent(Event event)
{
GC gc = event.gc;
gc.drawImage(image, 0, 0);
gc.setAlpha(30);
int interval = image.getBounds().height;
for (int i = 0; i < 10; i++)
{
int y = (int) Math.floor(i * (interval / 10.0));
gc.drawLine(0, y, image.getBounds().width, y);
}
interval = image.getBounds().width;
for (int i = 0; i < 10; i++)
{
int x = (int) Math.floor(i * (interval / 10.0));
gc.drawLine(x, 0, x, image.getBounds().width);
}
gc.setAlpha(255);
}
});
shell.pack();
shell.open();
while (!shell.isDisposed())
{
if (!display.readAndDispatch())
display.sleep();
}
display.dispose();
image.dispose();
}
Looks like this:
Related
I found a custom SWT-Column-Ratio Layout on the internet which puts the children of a composite/control into a user-defined ratio. Unfortunately I cannot find the source of the implementation of the Column-Ratio Layout, but here's how the code looks like:
public class ColumnRatioLayout extends Layout {
int[] percentages;
public ColumnRatioLayout(int... percentages) {
this.percentages = percentages;
}
#Override
protected Point computeSize(Composite composite, int wHint, int hHint, boolean flushCache) {
Control[] children = composite.getChildren();
int height = hHint;
int width = wHint;
int consumedPercent = 0;
for (int i = 0; i < children.length; i++) {
int percent = 0;
calculatePercentAndConsumedPercent(percent, consumedPercent, children, i);
Point childSize = children[i].computeSize(wHint == -1 ? -1 : wHint * percent / 100, hHint);
if (wHint == SWT.DEFAULT) {
width = Math.max(width, childSize.x * (100 - percent) / 100);
}
if (hHint == SWT.DEFAULT) {
height = Math.max(height, childSize.y);
}
}
return new Point(width, Math.max(height, 0));
}
protected void calculatePercentAndConsumedPercent(int percent, int consumedPercent, Control[] children, int i) {
if (i >= percentages.length) {
percent = (100 - consumedPercent) / (children.length - percentages.length);
} else {
percent = percentages[i];
consumedPercent += percent;
}
}
#Override
protected void layout(Composite composite, boolean flushCache) {
Control[] children = composite.getChildren();
Rectangle available = composite.getClientArea();
int x = available.x;
int consumedPercent = 0;
for (int i = 0; i < children.length - 1; i++) {
int percent;
if (i >= percentages.length) {
percent = (100 - consumedPercent) / (children.length - percentages.length);
} else {
percent = percentages[i];
consumedPercent += percent;
}
int w = available.width * percent / 100;
children[i].setBounds(x, available.y, w, available.height);
x += w;
}
if (children.length > 0) {
children[children.length - 1].setBounds(x, available.y,
available.width - (x - available.x), available.height);
}
}
}
I want to test this layout. I am writing a JUnit test to test if the ratio is true when using this layot. I have done this, but it gives me no useful output - Point {0, 0}:
public class ColumnRatioLayoutTest {
private static Display _display;
private static Shell _shell;
private static Composite _comp;
#BeforeAll
public static void setUpAll() {
_display = new Display();
_shell = new Shell(_display);
_comp = new Composite(_shell, SWT.NONE);
}
#Test
public void setLayoutTest() {
int[] colRatio = {20, 80};
ColumnRatioLayout colLayout = new ColumnRatioLayout(colRatio);
_comp.setLayout(colLayout);
_comp.setSize(_comp.computeSize(SWT.DEFAULT, SWT.DEFAULT));
Composite comp1 = new Composite(_comp, SWT.NONE);
comp1.setLayout(new FillLayout());
comp1.setSize(comp1.computeSize(SWT.DEFAULT, SWT.DEFAULT));
Composite comp2 = new Composite(_comp, SWT.NONE);
comp2.setLayout(new FillLayout());
comp2.setSize(comp2.computeSize(SWT.DEFAULT, SWT.DEFAULT));
System.out.println("Comp1 size: " + _comp.getSize());
}
}
I basically want to compare the size of the two composites and see that one is 4 times the size of the other. This will fulfill my test. How do I do that? Thanks in advance.
You can test the layout like this:
public class ColumnRatioLayoutTest {
private Display display;
private Shell shell;
#BeforeEach
public void setUp() {
display = new Display();
shell = new Shell(display);
}
#AfterEach
public void tearDown() {
display.dispose();
}
#Test
public void testLayout() {
shell.setSize(shell.computeSize(100, SWT.DEFAULT));
Control control20 = new Label(shell, SWT.NONE);
Control control80 = new Label(shell, SWT.NONE);
shell.setLayout(new ColumnRatioLayout(20, 80));
shell.layout();
assertEquals(100, shell.getSize().x);
assertEquals(20, control20.getSize().x);
assertEquals(80, control80.getSize().x);
}
}
The test creates a shell with a client area width of 100 pixels and then ensures that two controls that should occupy 20% and 80% of the width actually are 20 and 80 pixels wide.
There is no need to declare static Display and Shell, re-creating them for each test ensures that tests remain isolated.
BTW, widgets that are managed by a layout must not call setSize or otherwise modify their bounds, i.e. your code must not call comp1.setSize(...);
And, please, follow the Java Naming conventions, don't prefix variables with underscores
I have been working on a project that is displaying a grid 16 x 16 of images, based on user interaction this grid follows the user on a dynamically larger base (an example would be a base that is 50 x 50) than the 16 x 16 display.
However, I am using JLabel components to display these images, and every time the user interacts I have to move each of the 256 images and erase the ones that are no longer in the 16 x 16 display grid. This results in a lag that is close to a second per key press and is close to nonfunctional.
What I am looking to try to do is to chain these images together in the total width of the ground and simply move the focus to the portion that is within the 16 x 16 grid, making the process no longer have to use nested for loops for the display.
Is it possible that I could dynamically store and create these chained images for display using a label? If are there other ways to display .png files in Java that could be stored and used in a similar manner?
An example of my current methodology of having to draw every image upon every user interaction:
User user = game.user;
int floorWidth = game.floorWidth;
int floorHeight = game.floorHeight;
int pX = user.getTile().getX();
int pY = user.getTile().getY();
int minX = Math.max(pX - GameConstants.USER_DRAW_DISTANCE, 0);
int maxX = Math.min(floorWidth, pX + GameConstants.USER_DRAW_DISTANCE);
int minY = Math.max(pY - GameConstants.USER_DRAW_DISTANCE, 0);
int maxY = Math.min(floorHeight, pY + GameConstants.USER_DRAW_DISTANCE);
for (int i = minY; i < maxY; i++)
{
for (int x = minX; x < maxX; x++)
{
Tile tile = floor.getTile(x, i);
if (tile.getHasSeen())
{
JLabel cLabel = tile.imageLabel;
cLabel.setLocation(340 + x * 32, 140 + i * 32);
cLabel.setSize(64, 64);
cLabel.setVisible(true);
panel.add(cLabel, 1);
}
}
}
In principle your idea should work. So you're probably doing something else wrong.
I've made an example, where it displays a 16x16 square of JLabels out of 256x256 JLabels. When you move the mouse over the panel, it changes the layout to show a new set of 16x16 JLabels. The change is pretty snappy, definitely not a 1 second delay.
import javax.swing.*;
import java.awt.EventQueue;
import java.awt.Dimension;
import java.awt.Color;
import java.awt.event.*;
import java.util.*;
public class GridViewer{
int x0, y0;
int N = 256;
int display = 16;
int length = 32;
List<JLabel> showing = new ArrayList<>();
List<JLabel> available = new ArrayList<>();
JPanel panel = new JPanel(){
Dimension sz = new Dimension(length*display, length*display);
#Override
public Dimension getPreferredSize(){
return sz;
}
};
public void showGui(){
JFrame frame = new JFrame();
panel.setLayout(null);
panel.addMouseMotionListener( new MouseAdapter(){
Random r = new Random();
#Override
public void mouseMoved(MouseEvent evt){
int x = evt.getX();
int y = evt.getY();
//map to position on the image to the position on the grid.
x0 = x/2;
x0 = Math.min(x0, N-display);
y0 = y/2;
y0 = Math.min(y0, N-display);
updateLayout();
}
});
for(int i = 0; i<N*N; i++){
available.add(createItem(i));
}
updateLayout();
frame.setContentPane(panel);
frame.pack();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
/**
* Creates a solid color jlabel, could be used to load an image
* as an icon.
**/
JLabel createItem(int i){
JLabel l = new JLabel("");
int r = (i/256);
int g = (0)&255;
int b = (i%256);
int c = (r << 16 ) + ( g << 8 ) + b;
l.setBackground(new Color(c));
l.setOpaque(true);
l.setSize(length, length);
return l;
}
public void updateLayout(){
for(JLabel l: showing){
panel.remove(l);
}
for(int i = 0; i<display; i++){
for(int j = 0; j<display; j++){
JLabel l = available.get((i + x0) + (j+y0)*N);
panel.add(l);
l.setLocation( i*length, j*length);
showing.add(l);
}
}
}
public static void main(String[] args){
EventQueue.invokeLater( () -> new GridViewer().showGui() );
}
}
Some variations.
Use a GridLayout
Using a layout manager has a lot of advantages. Especially when it comes to using different displays, fonts and platforms? When adding and removing elements, it could make partially showing elements tough.
Use a large JPanel with a ScrollPane
We could create a single JPanel and add all 256x256 components to it, then use a scroll pane to set the view. This would have an advantage of completely separating the layout and the view. Somebody wants a larger window, you don't have to change the layout, the view gets bigger and you just see more of the layout. For 256x256 components, it should perform well but if you have too many components you might want to reconsider it.
Use a JPanel and override paintComponent
This would involve loading your 'png' files as awt Images (probably BufferedImages) and drawing them with the graphics object. You would need to handle all of the layout and rendering. It gives you quite a bit of power over how you want to render your components.
So the case is that I have a SWT composite A which aggregates composite B and C.
The content of B and C consists of multiple rows that consists of Label and Text.
Within composite B or C rows are aligned properly ( you can draw a straight vertical line in the place where label border ends and text starts). But if you compare B and C then C content looks like it is indented against B.
For example:
Does anyone have an idea how to achieve it ?
The only way I can think of to align the first column of each Composite is to set the GridData#widthHint to the same value. This value would have to be the maximal width of any of the elements in the first column.
I gave it a try and came up with this solution (it's not optimized and consequently might not be the most efficient way to do it):
private static Random random = new Random(System.currentTimeMillis());
public static void main(String[] args)
{
Display display = new Display();
Shell shell = new Shell(display);
shell.setText("StackOverflow");
shell.setLayout(new GridLayout(1, false));
Composite first = createComposite(shell);
Composite second = createComposite(shell);
synchronizeFirstColumn(2, first, second);
shell.pack();
shell.open();
while (!shell.isDisposed())
{
if (!display.readAndDispatch())
{
display.sleep();
}
}
display.dispose();
}
private static Composite createComposite(Shell shell)
{
Composite comp = new Composite(shell, SWT.BORDER);
comp.setLayout(new GridLayout(2, false));
comp.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
for (int i = 0; i < 3; i++)
{
String content = UUID.randomUUID().toString();
content = content.substring(0, Math.round(random.nextFloat() * content.length()));
Label label = new Label(comp, SWT.RIGHT);
label.setText(content);
label.setLayoutData(new GridData(SWT.END, SWT.TOP, false, false));
Text text = new Text(comp, SWT.BORDER);
text.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false));
}
return comp;
}
private static void synchronizeFirstColumn(int nrOfColumns, Composite... comps)
{
if (comps == null || comps.length == 0)
return;
int maxWidth = 0;
for (Composite comp : comps)
{
Control[] controls = comp.getChildren();
for (int i = 0; i < controls.length; i += nrOfColumns)
{
int width = controls[i].computeSize(SWT.DEFAULT, SWT.DEFAULT).x;
if (width > maxWidth)
maxWidth = width;
}
}
for (Composite comp : comps)
{
Control[] controls = comp.getChildren();
for (int i = 0; i < controls.length; i += nrOfColumns)
{
Object data = controls[i].getLayoutData();
if(data instanceof GridData)
{
GridData grid = (GridData) data;
grid.widthHint = maxWidth;
}
}
}
}
Looks like this:
I'm trying to code a zoom-able image in a JScrollPane.
When the image is fully zoomed out it should be centered horizontally and vertically. When both scroll bars have appeared the zooming should always happen relative to the mouse coordinate, i.e. the same point of the image should be under the mouse before and after the zoom event.
I have almost achieves my goal. Unfortunately the "scrollPane.getViewport().setViewPosition()" method sometimes fails to update the view position correctly. Calling the method twice (hack!) overcomes the issue in most cases, but the view still flickers.
I have no explanation as to why this is happening. However I'm confident that it's not a math problem.
Below is a MWE. To see what my problem is in particular you can do the following:
Zoom in until you have some scroll bars (200% zoom or so)
Scroll into the bottom right corner by clicking the scroll bars
Place the mouse in the corner and zoom in twice. The second time you'll see how the scroll position jumps towards the center.
I would really appreciate if someone could tell me where the problem lies. Thank you!
package com.vitco;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseWheelEvent;
import java.awt.image.BufferedImage;
import java.util.Random;
/**
* Zoom-able scroll panel test case
*/
public class ZoomScrollPanel {
// the size of our image
private final static int IMAGE_SIZE = 600;
// create an image to display
private BufferedImage getImage() {
BufferedImage image = new BufferedImage(IMAGE_SIZE, IMAGE_SIZE, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
// draw the small pixel first
Random rand = new Random();
for (int x = 0; x < IMAGE_SIZE; x += 10) {
for (int y = 0; y < IMAGE_SIZE; y += 10) {
g.setColor(new Color(rand.nextInt(255),rand.nextInt(255),rand.nextInt(255)));
g.fillRect(x, y, 10, 10);
}
}
// draw the larger transparent pixel second
for (int x = 0; x < IMAGE_SIZE; x += 100) {
for (int y = 0; y < IMAGE_SIZE; y += 100) {
g.setColor(new Color(rand.nextInt(255),rand.nextInt(255),rand.nextInt(255), 180));
g.fillRect(x, y, 100, 100);
}
}
return image;
}
// the image panel that resizes according to zoom level
private class ImagePanel extends JPanel {
private final BufferedImage image = getImage();
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D)g.create();
g2.scale(scale, scale);
g2.drawImage(image, 0, 0, null);
g2.dispose();
}
#Override
public Dimension getPreferredSize() {
return new Dimension((int)Math.round(IMAGE_SIZE * scale), (int)Math.round(IMAGE_SIZE * scale));
}
}
// the current zoom level (100 means the image is shown in original size)
private double zoom = 100;
// the current scale (scale = zoom/100)
private double scale = 1;
// the last seen scale
private double lastScale = 1;
public void alignViewPort(Point mousePosition) {
// if the scale didn't change there is nothing we should do
if (scale != lastScale) {
// compute the factor by that the image zoom has changed
double scaleChange = scale / lastScale;
// compute the scaled mouse position
Point scaledMousePosition = new Point(
(int)Math.round(mousePosition.x * scaleChange),
(int)Math.round(mousePosition.y * scaleChange)
);
// retrieve the current viewport position
Point viewportPosition = scrollPane.getViewport().getViewPosition();
// compute the new viewport position
Point newViewportPosition = new Point(
viewportPosition.x + scaledMousePosition.x - mousePosition.x,
viewportPosition.y + scaledMousePosition.y - mousePosition.y
);
// update the viewport position
// IMPORTANT: This call doesn't always update the viewport position. If the call is made twice
// it works correctly. However the screen still "flickers".
scrollPane.getViewport().setViewPosition(newViewportPosition);
// debug
if (!newViewportPosition.equals(scrollPane.getViewport().getViewPosition())) {
System.out.println("Error: " + newViewportPosition + " != " + scrollPane.getViewport().getViewPosition());
}
// remember the last scale
lastScale = scale;
}
}
// reference to the scroll pane container
private final JScrollPane scrollPane;
// constructor
public ZoomScrollPanel() {
// initialize the frame
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setSize(600, 600);
// initialize the components
final ImagePanel imagePanel = new ImagePanel();
final JPanel centerPanel = new JPanel();
centerPanel.setLayout(new GridBagLayout());
centerPanel.add(imagePanel);
scrollPane = new JScrollPane(centerPanel);
scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
frame.add(scrollPane);
// add mouse wheel listener
imagePanel.addMouseWheelListener(new MouseAdapter() {
#Override
public void mouseWheelMoved(MouseWheelEvent e) {
super.mouseWheelMoved(e);
// check the rotation of the mousewheel
int rotation = e.getWheelRotation();
boolean zoomed = false;
if (rotation > 0) {
// only zoom out until no scrollbars are visible
if (scrollPane.getHeight() < imagePanel.getPreferredSize().getHeight() ||
scrollPane.getWidth() < imagePanel.getPreferredSize().getWidth()) {
zoom = zoom / 1.3;
zoomed = true;
}
} else {
// zoom in until maximum zoom size is reached
double newCurrentZoom = zoom * 1.3;
if (newCurrentZoom < 1000) { // 1000 ~ 10 times zoom
zoom = newCurrentZoom;
zoomed = true;
}
}
// check if a zoom happened
if (zoomed) {
// compute the scale
scale = (float) (zoom / 100f);
// align our viewport
alignViewPort(e.getPoint());
// invalidate and repaint to update components
imagePanel.revalidate();
scrollPane.repaint();
}
}
});
// display our frame
frame.setVisible(true);
}
// the main method
public static void main(String[] args) {
new ZoomScrollPanel();
}
}
Note: I have also looked at the question here JScrollPane setViewPosition After "Zoom" but unfortunately the problem and solution are slightly different and do not apply.
Edit
I have solved the issue by using a hack, however I'm still no closer to understanding as to what the underlying problem is. What is happening is that when the setViewPosition is called some internal state changes trigger additional calls to setViewPosition. These additional calls only happen occasionally. When I'm blocking them everything works perfectly.
To fix the problem I simply introduced a new boolean variable "blocked = false;" and replaced the lines
scrollPane = new JScrollPane(centerPanel);
and
scrollPane.getViewport().setViewPosition(newViewportPosition);
with
scrollPane = new JScrollPane();
scrollPane.setViewport(new JViewport() {
private boolean inCall = false;
#Override
public void setViewPosition(Point pos) {
if (!inCall || !blocked) {
inCall = true;
super.setViewPosition(pos);
inCall = false;
}
}
});
scrollPane.getViewport().add(centerPanel);
and
blocked = true;
scrollPane.getViewport().setViewPosition(newViewportPosition);
blocked = false;
I would still really appreciate if someone could make sense of this!
Why does this hack work? Is there a cleaner way to achieve the same functionality?
Here is the completed, fully functional Code. I still don't understand why the hack is necessary, but at least it now works as expected:
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseWheelEvent;
import java.awt.image.BufferedImage;
import java.util.Random;
/**
* Zoom-able scroll panel
*/
public class ZoomScrollPanel {
// the size of our image
private final static int IMAGE_SIZE = 600;
// create an image to display
private BufferedImage getImage() {
BufferedImage image = new BufferedImage(IMAGE_SIZE, IMAGE_SIZE, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
// draw the small pixel first
Random rand = new Random();
for (int x = 0; x < IMAGE_SIZE; x += 10) {
for (int y = 0; y < IMAGE_SIZE; y += 10) {
g.setColor(new Color(rand.nextInt(255),rand.nextInt(255),rand.nextInt(255)));
g.fillRect(x, y, 10, 10);
}
}
// draw the larger transparent pixel second
for (int x = 0; x < IMAGE_SIZE; x += 100) {
for (int y = 0; y < IMAGE_SIZE; y += 100) {
g.setColor(new Color(rand.nextInt(255),rand.nextInt(255),rand.nextInt(255), 180));
g.fillRect(x, y, 100, 100);
}
}
return image;
}
// the image panel that resizes according to zoom level
private class ImagePanel extends JPanel {
private final BufferedImage image = getImage();
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D)g.create();
g2.scale(scale, scale);
g2.drawImage(image, 0, 0, null);
g2.dispose();
}
#Override
public Dimension getPreferredSize() {
return new Dimension((int)Math.round(IMAGE_SIZE * scale), (int)Math.round(IMAGE_SIZE * scale));
}
}
// the current zoom level (100 means the image is shown in original size)
private double zoom = 100;
// the current scale (scale = zoom/100)
private double scale = 1;
// the last seen scale
private double lastScale = 1;
// true if currently executing setViewPosition
private boolean blocked = false;
public void alignViewPort(Point mousePosition) {
// if the scale didn't change there is nothing we should do
if (scale != lastScale) {
// compute the factor by that the image zoom has changed
double scaleChange = scale / lastScale;
// compute the scaled mouse position
Point scaledMousePosition = new Point(
(int)Math.round(mousePosition.x * scaleChange),
(int)Math.round(mousePosition.y * scaleChange)
);
// retrieve the current viewport position
Point viewportPosition = scrollPane.getViewport().getViewPosition();
// compute the new viewport position
Point newViewportPosition = new Point(
viewportPosition.x + scaledMousePosition.x - mousePosition.x,
viewportPosition.y + scaledMousePosition.y - mousePosition.y
);
// update the viewport position
blocked = true;
scrollPane.getViewport().setViewPosition(newViewportPosition);
blocked = false;
// remember the last scale
lastScale = scale;
}
}
// reference to the scroll pane container
private final JScrollPane scrollPane;
// constructor
public ZoomScrollPanel() {
// initialize the frame
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setSize(600, 600);
// initialize the components
final ImagePanel imagePanel = new ImagePanel();
final JPanel centerPanel = new JPanel();
centerPanel.setLayout(new GridBagLayout());
centerPanel.add(imagePanel);
scrollPane = new JScrollPane();
scrollPane.setViewport(new JViewport() {
private boolean inCall = false;
#Override
public void setViewPosition(Point pos) {
if (!inCall || !blocked) {
inCall = true;
super.setViewPosition(pos);
inCall = false;
}
}
});
scrollPane.getViewport().add(centerPanel);
scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
frame.add(scrollPane);
// add mouse wheel listener
imagePanel.addMouseWheelListener(new MouseAdapter() {
#Override
public void mouseWheelMoved(MouseWheelEvent e) {
super.mouseWheelMoved(e);
// check the rotation of the mousewheel
int rotation = e.getWheelRotation();
boolean zoomed = false;
if (rotation > 0) {
// only zoom out until no scrollbars are visible
if (scrollPane.getHeight() < imagePanel.getPreferredSize().getHeight() ||
scrollPane.getWidth() < imagePanel.getPreferredSize().getWidth()) {
zoom = zoom / 1.3;
zoomed = true;
}
} else {
// zoom in until maximum zoom size is reached
double newCurrentZoom = zoom * 1.3;
if (newCurrentZoom < 1000) { // 1000 ~ 10 times zoom
zoom = newCurrentZoom;
zoomed = true;
}
}
// check if a zoom happened
if (zoomed) {
// compute the scale
scale = (float) (zoom / 100f);
// align our viewport
alignViewPort(e.getPoint());
// invalidate and repaint to update components
imagePanel.revalidate();
scrollPane.repaint();
}
}
});
// display our frame
frame.setVisible(true);
}
// the main method
public static void main(String[] args) {
new ZoomScrollPanel();
}
}
Some time ago I was facing the same issue. I had some scalable/zoomable content (SWT widgets) stored in Viewport in JScrollPane and some features implemented to enable panning and zooming the content. I didn't look into your code if it's basically the same, but the issue that I was observing was completely the same. When zooming outside from the right/bottom side, sometimes, the view position jumped a little bit into the center (from my point-of-view that definitely points to a scale factor). Using doubled "setViewPosition" somehow enhanced the behavior but still not usable.
After some investigation, I've found out that the issue on my side was between the moment when I changed the scale factor of the content inside the scroll panel and the moment when view position was set in scroll panel. The thing is that scroll panel doesn't know about the content size updates until layout is done. So basically, it's updating the position based on old content size, extent size and view position.
So, at my side, this helped a lot.
// updating scroll panel content scale goes here
viewport.doLayout();
// setting view position in viewport goes here
Checking method BasicScrollPaneUI#syncScrollPaneWithViewport() was very useful on my side.
very useful example, excellent zoom at mouse pointer, here is the same code slightly modified to include mouse panning:
original code added taken from --> Scroll JScrollPane by dragging mouse (Java swing)
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.image.BufferedImage;
import java.util.Random;
/**
* Zoom-able scroll panel
*/
// https://stackoverflow.com/questions/22649636/zoomable-jscrollpane-setviewposition-fails-to-update
public class ZoomPanScrollPanel {
// the size of our image
private final static int IMAGE_SIZE = 1600;
// create an image to display
private BufferedImage getImage() {
BufferedImage image = new BufferedImage(IMAGE_SIZE, IMAGE_SIZE, BufferedImage.TYPE_INT_ARGB);
Graphics g = image.getGraphics();
// draw the small pixel first
Random rand = new Random();
for (int x = 0; x < IMAGE_SIZE; x += 10) {
for (int y = 0; y < IMAGE_SIZE; y += 10) {
g.setColor(new Color(rand.nextInt(255),rand.nextInt(255),rand.nextInt(255), rand.nextInt(255)));
g.fillRect(x, y, 10, 10);
}
}
// draw the larger transparent pixel second
for (int x = 0; x < IMAGE_SIZE; x += 100) {
for (int y = 0; y < IMAGE_SIZE; y += 100) {
g.setColor(new Color(rand.nextInt(255),rand.nextInt(255),rand.nextInt(255), 180));
g.fillRect(x, y, 100, 100);
}
}
return image;
}
// the image panel that resizes according to zoom level
private class ImagePanel extends JPanel {
private final BufferedImage image = getImage();
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D)g.create();
g2.scale(scale, scale);
g2.drawImage(image, 0, 0, null);
g2.dispose();
}
#Override
public Dimension getPreferredSize() {
return new Dimension((int)Math.round(IMAGE_SIZE * scale), (int)Math.round(IMAGE_SIZE * scale));
}
}
// the current zoom level (100 means the image is shown in original size)
private double zoom = 100;
// the current scale (scale = zoom/100)
private double scale = 1;
// the last seen scale
private double lastScale = 1;
// true if currently executing setViewPosition
private boolean blocked = false;
public void alignViewPort(Point mousePosition) {
// if the scale didn't change there is nothing we should do
if (scale != lastScale) {
// compute the factor by that the image zoom has changed
double scaleChange = scale / lastScale;
// compute the scaled mouse position
Point scaledMousePosition = new Point(
(int)Math.round(mousePosition.x * scaleChange),
(int)Math.round(mousePosition.y * scaleChange)
);
// retrieve the current viewport position
Point viewportPosition = scrollPane.getViewport().getViewPosition();
// compute the new viewport position
Point newViewportPosition = new Point(
viewportPosition.x + scaledMousePosition.x - mousePosition.x,
viewportPosition.y + scaledMousePosition.y - mousePosition.y
);
// update the viewport position
blocked = true;
scrollPane.getViewport().setViewPosition(newViewportPosition);
blocked = false;
// remember the last scale
lastScale = scale;
}
}
// reference to the scroll pane container
private final JScrollPane scrollPane;
// constructor
public ZoomPanScrollPanel() {
// initialize the frame
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setSize(600, 600);
// initialize the components
final ImagePanel imagePanel = new ImagePanel();
final JPanel centerPanel = new JPanel();
centerPanel.setLayout(new GridBagLayout());
centerPanel.add(imagePanel);
scrollPane = new JScrollPane();
scrollPane.setViewport(new JViewport() {
private boolean inCall = false;
#Override
public void setViewPosition(Point pos) {
if (!inCall || !blocked) {
inCall = true;
super.setViewPosition(pos);
inCall = false;
}
}
});
scrollPane.getViewport().add(centerPanel);
scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
frame.add(scrollPane);
// add mouse wheel listener
imagePanel.addMouseWheelListener(new MouseAdapter() {
#Override
public void mouseWheelMoved(MouseWheelEvent e) {
super.mouseWheelMoved(e);
// check the rotation of the mousewheel
int rotation = e.getWheelRotation();
boolean zoomed = false;
if (rotation > 0) {
// only zoom out until no scrollbars are visible
if (scrollPane.getHeight() < imagePanel.getPreferredSize().getHeight() ||
scrollPane.getWidth() < imagePanel.getPreferredSize().getWidth()) {
zoom = zoom / 1.3;
zoomed = true;
}
} else {
// zoom in until maximum zoom size is reached
double newCurrentZoom = zoom * 1.3;
if (newCurrentZoom < 1000) { // 1000 ~ 10 times zoom
zoom = newCurrentZoom;
zoomed = true;
}
}
// check if a zoom happened
if (zoomed) {
// compute the scale
scale = (float) (zoom / 100f);
// align our viewport
alignViewPort(e.getPoint());
// invalidate and repaint to update components
imagePanel.revalidate();
scrollPane.repaint();
}
}
});
//mouse panning
//original code: https://stackoverflow.com/questions/31171502/scroll-jscrollpane-by-dragging-mouse-java-swing
MouseAdapter ma = new MouseAdapter() {
private Point origin;
#Override
public void mousePressed(MouseEvent e) {
origin = new Point(e.getPoint());
}
#Override
public void mouseReleased(MouseEvent e) {
}
#Override
public void mouseDragged(MouseEvent e) {
if (origin != null) {
JViewport viewPort = (JViewport) SwingUtilities.getAncestorOfClass(JViewport.class, imagePanel);
if (viewPort != null) {
int deltaX = origin.x - e.getX();
int deltaY = origin.y - e.getY();
System.out.println("X pan = "+ deltaX);
System.out.println("Y pan = "+ deltaY);
Rectangle view = viewPort.getViewRect();
view.x += deltaX;
view.y += deltaY;
imagePanel.scrollRectToVisible(view);
}
}
}
};
imagePanel.addMouseListener(ma);
imagePanel.addMouseMotionListener(ma);
imagePanel.setAutoscrolls(true);
// display our frame
frame.setVisible(true);
}
// the main method
public static void main(String[] args) {
new ZoomPanScrollPanel();
}
}
The task: Open a window using Java SWT above all other windows (imagine yourselves hitting Notepad , then after executing Notepad.exe , the window is opened above all other windows.
The problem:
I'm using Java SWT for GUI , and each time that I open a window , the opened window appears beneath all other windows .
Given the code :
Display display = new Display();
shell = new Shell(display);
shell.setSize(750,750);
Monitor primary = display.getPrimaryMonitor();
Rectangle bounds = primary.getBounds();
Rectangle rect = shell.getBounds();
int x = bounds.x + (bounds.width - rect.width) / 2;
int y = bounds.y + (bounds.height - rect.height) / 2;
shell.setLocation(x, y);
roomColor = display.getSystemColor(SWT.COLOR_DARK_GREEN);
wallColor = display.getSystemColor(SWT.COLOR_RED);
doorColor = display.getSystemColor(SWT.COLOR_BLUE);
shell.setText("Maze");
createContents(shell,maze);
shell.open();
while (!shell.isDisposed())
{
if (!display.readAndDispatch())
{
display.sleep();
}
}
display.dispose();
....
....
public void createContents(Shell shell,final MazeInterface maze)
{
FillLayout myLayout = new FillLayout();
myLayout.type = SWT.VERTICAL;
shell.setLayout(myLayout);
Canvas canvas = new Canvas(shell,SWT.NONE);
canvas.setSize(shell.getSize());
canvas.addPaintListener(new PaintListener()
{
public void paintControl(PaintEvent e)
{
prepareMazeDrawing(e,maze);
}
});
}
I've tried countless changes but the window is still opened under all other windows.
I'd appreciate your help
Regards,Ron