JavaFX Border don't fit for node with custom shape - java

I'm trying to figure out, if it is possible to draw a border for a node with a custom shape. Currently the border doesn't fit the shape of the node.
This is what it currently looks like:
The shape is achieved by the following CSS:
.arrow-tail {
-fx-shape: "M 0 0 L 10 0 L 10 10 L 0 10 L 10 5 Z";
}
.arrow-head {
-fx-shape: "M 0 0 L 10 5 L 0 10 Z";
}
This is the important code of the arrow class where the CSS is used:
public class Arrow extends HBox {
public void Arrow(Node graphic, String title) {
getChildren().addAll(getArrowTail(), getArrowMiddlePart(graphic, title), getArrowHead());
}
private final Region getArrowTail() {
final Region arrowTail = new Region();
arrowTail.setMinWidth(10);
arrowTail.getStyleClass().add("arrow-tail");
return arrowTail;
}
private final Node getArrowMiddlePart(Node graphic, String text) {
labelTitle = new Label(text);
labelTitle.setGraphic(graphic);
labelTitle.idProperty().bind(idProperty());
final Tooltip tooltip = new Tooltip();
tooltip.textProperty().bind(labelTitle.textProperty());
Tooltip.install(labelTitle, tooltip);
final HBox arrowMiddlePart = new HBox(labelTitle);
arrowMiddlePart.minWidthProperty().bind(minWidthProperty());
arrowMiddlePart.setAlignment(Pos.CENTER);
return arrowMiddlePart;
}
private final Region getArrowHead() {
final Region arrowHead = new Region();
arrowHead.setMinWidth(10);
arrowHead.getStyleClass().add("arrow-head");
return arrowHead;
}
}
The Arrow class is a HBox, where I create a custom shaped region as arrow tail and arrow head and another HBox with an label in it as arrow middle part.

Unfortunately there doesn't seem to be a way to set the borders for the different sides of a Region with a shape applied to it independently.
I recommend extending Region directly adding a Path as first child and overriding layoutChildren resize this path.
public class Arrow extends Region {
private static final double ARROW_LENGTH = 10;
private static final Insets MARGIN = new Insets(1, ARROW_LENGTH, 1, ARROW_LENGTH);
private final HBox container;
private final HLineTo hLineTop;
private final LineTo tipTop;
private final LineTo tipBottom;
private final LineTo tailBottom;
public Arrow(Node graphic, String title) {
Path path = new Path(
new MoveTo(),
hLineTop = new HLineTo(),
tipTop = new LineTo(ARROW_LENGTH, 0),
tipBottom = new LineTo(-ARROW_LENGTH, 0),
new HLineTo(),
tailBottom = new LineTo(ARROW_LENGTH, 0),
new ClosePath());
tipTop.setAbsolute(false);
tipBottom.setAbsolute(false);
path.setManaged(false);
path.setStrokeType(StrokeType.INSIDE);
path.getStyleClass().add("arrow-shape");
Label labelTitle = new Label(title, graphic);
container = new HBox(labelTitle);
getChildren().addAll(path, container);
HBox.setHgrow(labelTitle, Priority.ALWAYS);
labelTitle.setAlignment(Pos.CENTER);
labelTitle.setMaxWidth(Double.POSITIVE_INFINITY);
}
#Override
protected void layoutChildren() {
// hbox layout
Insets insets = getInsets();
double left = insets.getLeft();
double top = insets.getTop();
double width = getWidth();
double height = getHeight();
layoutInArea(container,
left, top,
width - left - insets.getRight(), height - top - insets.getBottom(),
0, MARGIN, true, true, HPos.LEFT, VPos.TOP);
// adjust arrow shape
double length = width - ARROW_LENGTH;
double h2 = height / 2;
hLineTop.setX(length);
tipTop.setY(h2);
tipBottom.setY(h2);
tailBottom.setY(h2);
}
#Override
protected double computeMinWidth(double height) {
Insets insets = getInsets();
return 2 * ARROW_LENGTH + insets.getLeft() + insets.getRight() + container.minWidth(height);
}
#Override
protected double computeMinHeight(double width) {
Insets insets = getInsets();
return 2 + insets.getTop() + insets.getBottom() + container.minHeight(width);
}
#Override
protected double computePrefWidth(double height) {
Insets insets = getInsets();
return 2 * ARROW_LENGTH + insets.getLeft() + insets.getRight() + container.prefWidth(height);
}
#Override
protected double computePrefHeight(double width) {
Insets insets = getInsets();
return 2 + insets.getTop() + insets.getBottom() + container.prefHeight(width);
}
#Override
protected double computeMaxWidth(double height) {
Insets insets = getInsets();
return 2 * ARROW_LENGTH + insets.getLeft() + insets.getRight() + container.maxWidth(height);
}
#Override
protected double computeMaxHeight(double width) {
Insets insets = getInsets();
return 2 + insets.getTop() + insets.getBottom() + container.maxHeight(width);
}
}
CSS
.arrow-shape {
-fx-fill: dodgerblue;
-fx-stroke: black;
}
Note that the code would be simpler, if you extend HBox, but this would allow other classes access to the child list which could result in removal of the Path; extending Region allows us to keep the method protected preventing this kind of access but requires us to implement the compute... methods and the layouting of the children.

Related

Creating a simple custom scroll view

This is somehow a general question about scroll views, I want to learn the basics of a scroll view and how to implement one on my own because it is essential as part of most dynamic GUI. You may ask, Why not simply use the one provided by the platform? My answer would be, aside from it's fun to learn new stuff, it's nice to see things customized the way you want it to be. Simply put, I want to create just a simple custom scroll view and try to understand how it is working behind the scene.
Moving on, what I currently have to present here is just the simplest example of the UI I came up with. Basically, it is a Pane that serves as the viewport for the entire content and contains a single vertical scrollbar on its right edge, just like normal scroll views, but I just added a little transition which animates the scrollbar's width on mouse hover.
ScrollContainer class
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
/**
* ScrollContainer
*
* A container for scrolling large content.
*/
public class ScrollContainer extends Pane {
private VerticalScrollBar scrollBar; // The scrollbar used for scrolling over the content from viewport
private Rectangle rectangle; // Object for clipping the viewport to restrict overflowing content
/**
* Construct a new ScrollContainer
*/
public ScrollContainer() {
super();
scrollBar = new VerticalScrollBar();
getChildren().add(scrollBar);
rectangle = new Rectangle();
rectangle.widthProperty().bind(widthProperty());
rectangle.heightProperty().bind(heightProperty());
setClip(rectangle);
}
#Override
protected void layoutChildren() {
super.layoutChildren();
// Layout scrollbar to the edge of container, and fit the viewport's height as well
scrollBar.resize(scrollBar.getWidth(), getHeight());
scrollBar.setLayoutX(getWidth() - scrollBar.getWidth());
}
/**
* VerticalScrollBar
*/
private class VerticalScrollBar extends Region {
// Temporary scrubber's height.
// TODO: Figure out the computation for scrubber's height.
private static final double SCRUBBER_LENGTH = 100;
private double initialY; // Initial mouse position when dragging the scrubber
private Timeline widthTransition; // Transforms width of scrollbar on hover
private Region scrubber; // Indicator about the content's visible area
/**
* Construct a new VerticalScrollBar
*/
private VerticalScrollBar() {
super();
// Scrollbar's initial width
setPrefWidth(7);
widthTransition = new Timeline(
new KeyFrame(Duration.ZERO, new KeyValue(prefWidthProperty(), 7)),
new KeyFrame(Duration.millis(500), new KeyValue(prefWidthProperty(), 14))
);
scrubber = new Region();
scrubber.setStyle("-fx-background-color: rgba(0,0,0, 0.25)");
scrubber.setOnMousePressed(event -> initialY = event.getY());
scrubber.setOnMouseDragged(event -> {
// Moves the scrubber vertically within the scrollbar.
// TODO: Figure out the proper way of handling scrubber movement, an onScroll mouse wheel function, ect.
double initialScrollY = event.getSceneY() - initialY;
double maxScrollY = getHeight() - SCRUBBER_LENGTH;
double minScrollY = 0;
if (initialScrollY >= minScrollY && initialScrollY <= maxScrollY) {
scrubber.setTranslateY(initialScrollY);
}
});
getChildren().add(scrubber);
// Animate scrollbar's width on mouse enter and exit
setOnMouseEntered(event -> {
widthTransition.setRate(1);
widthTransition.play();
});
setOnMouseExited(event -> {
widthTransition.setRate(-1);
widthTransition.play();
});
}
#Override
protected void layoutChildren() {
super.layoutChildren();
// Layout scrubber to fit the scrollbar's width
scrubber.resize(getWidth(), SCRUBBER_LENGTH);
}
}
}
Main class
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
Label lorem = new Label();
lorem.setStyle("-fx-padding: 20px;");
lorem.setText("Lorem ipsum dolor sit amet, consectetur adipiscing elit. " +
"Integer ut ornare enim, a rutrum nisl. " +
"Proin eros felis, rutrum at pharetra viverra, elementum quis lacus. " +
"Nam sit amet sollicitudin nibh, ac mattis lectus. " +
"Sed mattis ullamcorper sapien, a pulvinar turpis hendrerit vel. " +
"Fusce nec diam metus. In vel dui lacus. " +
"Sed imperdiet ipsum euismod aliquam rhoncus. " +
"Morbi sagittis mauris ac massa pretium, vel placerat purus porta. " +
"Suspendisse orci leo, sagittis eu orci vitae, porttitor sagittis odio. " +
"Proin iaculis enim sed ipsum sodales, at congue ante blandit. " +
"Etiam mattis erat nec dolor vestibulum, quis interdum sem pellentesque. " +
"Nullam accumsan ex non lacus sollicitudin interdum.");
lorem.setWrapText(true);
StackPane content = new StackPane();
content.setPrefSize(300, 300);
content.setMinSize(300, 300);
content.setMaxSize(300, 300);
content.setStyle("-fx-background-color: white;");
content.getChildren().add(lorem);
ScrollContainer viewport = new ScrollContainer();
viewport.setStyle("-fx-background-color: whitesmoke");
viewport.getChildren().add(0, content);
primaryStage.setScene(new Scene(viewport, 300, 150));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
I wanted to see a working example showing just the basic art of scrolling; like the proper way of handling the thumb's animation movement, computation of scrollbar's thumb length, and lastly, the required total unit or amount to move the content. I think these three parts are the keys to the core of a scroll view.
P.S
I also want to see the use of the onScroll event in JavaFX, right now all I know is the common used mouse events. Thank you in advance.
UPDATE
I've added a BlockIncrement function to the answer of sir #fabian below. It will basically just move the thumb to the current position of the pointer while keeping the [0, 1] range value. All credits and thanks goes to him.
This is for others who were looking for something like this idea of
custom scroll view, hope you might find this reference useful in the future.
public class ScrollContainer extends Region {
private VerticalScrollBar scrollBar; // The scrollbar used for scrolling over the content from viewport
private Rectangle rectangle; // Object for clipping the viewport to restrict overflowing content
/**
* Construct a new ScrollContainer
*/
public ScrollContainer() {
setOnScroll(evt -> {
double viewportHeight = getHeight();
double contentHeight = getContentHeight();
if (contentHeight > viewportHeight) {
double delta = evt.getDeltaY() / (viewportHeight - contentHeight);
if (Double.isFinite(delta)) {
scrollBar.setValue(scrollBar.getValue() + delta);
}
}
});
scrollBar = new VerticalScrollBar();
getChildren().add(scrollBar);
rectangle = new Rectangle();
setClip(rectangle);
}
private Node content;
public void setContent(Node content) {
if (this.content != null) {
// remove old content
getChildren().remove(this.content);
}
if (content != null) {
// add new content
getChildren().add(0, content);
}
this.content = content;
}
private double getContentHeight() {
return content == null ? 0 : content.getLayoutBounds().getHeight();
}
#Override
protected void layoutChildren() {
super.layoutChildren();
double w = getWidth();
double h = getHeight();
double sw = scrollBar.getWidth();
double viewportWidth = w - sw;
double viewportHeight = h;
if (content != null) {
double contentHeight = getContentHeight();
double vValue = scrollBar.getValue();
// position content according to scrollbar value
content.setLayoutY(Math.min(0, viewportHeight - contentHeight) * vValue);
}
// Layout scrollbar to the edge of container, and fit the viewport's height as well
scrollBar.resize(sw, h);
scrollBar.setLayoutX(viewportWidth);
// resize clip
rectangle.setWidth(w);
rectangle.setHeight(h);
}
/**
* VerticalScrollBar
*/
private class VerticalScrollBar extends Region {
private boolean thumbPressed; // Indicates that the scrubber was pressed
private double initialValue;
private double initialY; // Initial mouse position when dragging the scrubber
private Timeline widthTransition; // Transforms width of scrollbar on hover
private Region scrubber; // Indicator about the content's visible area
private double value;
private void setValue(double v) {
value = v;
}
private double getValue() {
return value;
}
private double calculateScrubberHeight() {
double h = getHeight();
return h * h / getContentHeight();
}
/**
* Construct a new VerticalScrollBar
*/
private VerticalScrollBar() {
// Scrollbar's initial width
setPrefWidth(7);
widthTransition = new Timeline(
new KeyFrame(Duration.ZERO, new KeyValue(prefWidthProperty(), 7)),
new KeyFrame(Duration.millis(500), new KeyValue(prefWidthProperty(), 14))
);
scrubber = new Region();
scrubber.setStyle("-fx-background-color: rgba(0,0,0, 0.25)");
scrubber.setOnMousePressed(event -> {
initialY = scrubber.localToParent(event.getX(), event.getY()).getY();
initialValue = value;
thumbPressed = true;
});
scrubber.setOnMouseDragged(event -> {
if (thumbPressed) {
double currentY = scrubber.localToParent(event.getX(), event.getY()).getY();
double sH = calculateScrubberHeight();
double h = getHeight();
// calculate value change and prevent errors
double delta = (currentY - initialY) / (h - sH);
if (!Double.isFinite(delta)) {
delta = 0;
}
// keep value in range [0, 1]
double newValue = Math.max(0, Math.min(1, initialValue + delta));
value = newValue;
// layout thumb
requestLayout();
}
});
scrubber.setOnMouseReleased(event -> thumbPressed = false);
getChildren().add(scrubber);
// Added BlockIncrement.
// Pressing the `track` or the scrollbar itself will move and position the
// scrubber to the pointer location, as well as the content prior to the
// value changes.
setOnMousePressed(event -> {
if (!thumbPressed) {
double sH = calculateScrubberHeight();
double h = getHeight();
double pointerY = event.getY();
double delta = pointerY / (h - sH);
double newValue = Math.max(0, Math.min(1, delta));
// keep value in range [0, 1]
if (delta > 1) {
newValue = 1;
}
value = newValue;
requestLayout();
}
});
// Animate scrollbar's width on mouse enter and exit
setOnMouseEntered(event -> {
widthTransition.setRate(1);
widthTransition.play();
});
setOnMouseExited(event -> {
widthTransition.setRate(-1);
widthTransition.play();
});
}
#Override
protected void layoutChildren() {
super.layoutChildren();
double h = getHeight();
double cH = getContentHeight();
if (cH <= h) {
// full size, if content does not excede viewport size
scrubber.resize(getWidth(), h);
} else {
double sH = calculateScrubberHeight();
// move thumb to position
scrubber.setTranslateY(value * (h - sH));
// Layout scrubber to fit the scrollbar's width
scrubber.resize(getWidth(), sH);
}
}
}
}
There are a few equations that allow you to compute the layout (all assuming contentHeight > viewportHeight):
vValue denotes the position of the thumb in the vertical scroll bar in [0, 1] (0 = topmost position, 1 = bottom of the thumb is at bottom of the track).
topY = vValue * (contentHeight - viewportHeight)
thumbHeight / trackHeight = viewportHeight / contentHeight
thumbY = vValue * (trackHeight - thumbHeight)
Also note that providing access to the children and adding the content outside of the ScrollContainer is bad practice since it requires the user of this class to do modifications that should be reserved for the class itself. Doing this could easily lead to the following line which breaks the ScrollContainer (the content could hide the thumb):
// viewport.getChildren().add(0, content);
viewport.getChildren().add(content);
It's better extend Region directly and using a method to (re)place the content.
public class ScrollContainer extends Region {
private VerticalScrollBar scrollBar; // The scrollbar used for scrolling over the content from viewport
private Rectangle rectangle; // Object for clipping the viewport to restrict overflowing content
/**
* Construct a new ScrollContainer
*/
public ScrollContainer() {
setOnScroll(evt -> {
double viewportHeight = getHeight();
double contentHeight = getContentHeight();
if (contentHeight > viewportHeight) {
double delta = evt.getDeltaY() / (viewportHeight - contentHeight);
if (Double.isFinite(delta)) {
scrollBar.setValue(scrollBar.getValue() + delta);
}
}
});
scrollBar = new VerticalScrollBar();
getChildren().add(scrollBar);
rectangle = new Rectangle();
setClip(rectangle);
}
private Node content;
public void setContent(Node content) {
if (this.content != null) {
// remove old content
getChildren().remove(this.content);
}
if (content != null) {
// add new content
getChildren().add(0, content);
}
this.content = content;
}
private double getContentHeight() {
return content == null ? 0 : content.getLayoutBounds().getHeight();
}
#Override
protected void layoutChildren() {
super.layoutChildren();
double w = getWidth();
double h = getHeight();
double sw = scrollBar.getWidth();
double viewportWidth = w - sw;
double viewportHeight = h;
if (content != null) {
double contentHeight = getContentHeight();
double vValue = scrollBar.getValue();
// position content according to scrollbar value
content.setLayoutY(Math.min(0, viewportHeight - contentHeight) * vValue);
}
// Layout scrollbar to the edge of container, and fit the viewport's height as well
scrollBar.resize(sw, h);
scrollBar.setLayoutX(viewportWidth);
// resize clip
rectangle.setWidth(w);
rectangle.setHeight(h);
}
/**
* VerticalScrollBar
*/
private class VerticalScrollBar extends Region {
private double initialValue;
private double initialY; // Initial mouse position when dragging the scrubber
private Timeline widthTransition; // Transforms width of scrollbar on hover
private Region scrubber; // Indicator about the content's visible area
private double value;
public double getValue() {
return value;
}
private double calculateScrubberHeight() {
double h = getHeight();
return h * h / getContentHeight();
}
/**
* Construct a new VerticalScrollBar
*/
private VerticalScrollBar() {
// Scrollbar's initial width
setPrefWidth(7);
widthTransition = new Timeline(
new KeyFrame(Duration.ZERO, new KeyValue(prefWidthProperty(), 7)),
new KeyFrame(Duration.millis(500), new KeyValue(prefWidthProperty(), 14))
);
scrubber = new Region();
scrubber.setStyle("-fx-background-color: rgba(0,0,0, 0.25)");
scrubber.setOnMousePressed(event -> {
initialY = scrubber.localToParent(event.getX(), event.getY()).getY();
initialValue = value;
});
scrubber.setOnMouseDragged(event -> {
double currentY = scrubber.localToParent(event.getX(), event.getY()).getY();
double sH = calculateScrubberHeight();
double h = getHeight();
// calculate value change and prevent errors
double delta = (currentY - initialY) / (h - sH);
if (!Double.isFinite(delta)) {
delta = 0;
}
// keep value in range [0, 1]
double newValue = Math.max(0, Math.min(1, initialValue + delta));
value = newValue;
// layout thumb
requestLayout();
});
getChildren().add(scrubber);
// Animate scrollbar's width on mouse enter and exit
setOnMouseEntered(event -> {
widthTransition.setRate(1);
widthTransition.play();
});
setOnMouseExited(event -> {
widthTransition.setRate(-1);
widthTransition.play();
});
}
#Override
protected void layoutChildren() {
super.layoutChildren();
double h = getHeight();
double cH = getContentHeight();
if (cH <= h) {
// full size, if content does not excede viewport size
scrubber.resize(getWidth(), h);
} else {
double sH = calculateScrubberHeight();
// move thumb to position
scrubber.setTranslateY(value * (h - sH));
// Layout scrubber to fit the scrollbar's width
scrubber.resize(getWidth(), sH);
}
}
}
}

Zoom on a JScrollPane with headers

I hava a JFrame containing a table with row and column headers.
My table is a custom component made of 3 panels (row header, column header and grid).
The panels are regular JPanels, containing either JButton or JLabel, in a MigLayout.
I display this component inside a JScrollPane in order to scroll simultaneously my grid and my headers.
This part works fine.
Now, the user should be able to zoom on my component.
I tried to use the pbjar JXLayer but if I put my whole JScrollPane inside the layer, everything is zoomed, event the scrollbars.
I tried to use 3 JXLayers, one for each viewPort of my JScrollPane. But this solution just mess up with my layout as the panels inside the viewPorts get centered instead of being top-left aligned.
import org.jdesktop.jxlayer.JXLayer;
import org.pbjar.jxlayer.demo.TransformUtils;
import org.pbjar.jxlayer.plaf.ext.transform.DefaultTransformModel;
public class Matrix extends JScrollPane {
private Grid grid;
private Header rowHeader;
private Header columnHeader;
private DefaultTransformModel zoomTransformModel;
private double zoom = 1;
public Matrix() {
super(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
this.zoomTransformModel1 = new DefaultTransformModel();
this.zoomTransformModel1.setScaleToPreferredSize(true);
this.zoomTransformModel1.setScale(1);
this.zoomTransformModel2 = new DefaultTransformModel();
this.zoomTransformModel2.setScaleToPreferredSize(true);
this.zoomTransformModel2.setScale(1);
this.zoomTransformModel3 = new DefaultTransformModel();
this.zoomTransformModel3.setScaleToPreferredSize(true);
this.zoomTransformModel3.setScale(1);
this.grid = new Grid();
this.setViewportView(TransformUtils.createTransformJXLayer(this.grid,
zoomTransformModel1););
this.matrixRowHeader = new Header(Orientation.VERTICAL);
this.setRowHeader(new JViewport(
TransformUtils.createTransformJXLayer(
this.rowHeader, zoomTransformModel2)));
this.matrixColumnHeader = new Header(Orientation.HORIZONTAL);
this.setColumnHeader(new JViewport(
TransformUtils.createTransformJXLayer(
this.columnHeader, zoomTransformModel2)));
}
public void setScale(double scale) {
this.zoomTransformModel1.setScale(scale);
this.zoomTransformModel2.setScale(scale);
this.zoomTransformModel3.setScale(scale);
}
}
How could I handle the zoom on my JScrollPane without zooming on the scrollBars and without messing up my layout?
First, MigLayout seems to be incompatible with the JXLayer. When using both, the components in the panel using the MigLayout have a unpredictable behaviour.
Then, the original pbjar JXLayer only allows you to put your component in the center of the Layer pane.
Pbjar sources can be download on github. Note this is not the official Piet Blok repository.
The solution I found is to modify the TransformLayout, TransformUI, and the TranformModel classes:
Alignment enum give the possible alignment for the component in the layer.
public enum Alignment {
TOP,
BOTTOM,
LEFT,
RIGHT,
CENTER
}
In TransformLayout :
#Override
public void layoutContainer(Container parent) {
JXLayer<?> layer = (JXLayer<?>) parent;
LayerUI<?> layerUI = layer.getUI();
if (layerUI instanceof CustomTransformUI) {
JComponent view = (JComponent) layer.getView();
JComponent glassPane = layer.getGlassPane();
if (view != null) {
Rectangle innerArea = new Rectangle();
SwingUtilities.calculateInnerArea(layer, innerArea);
view.setSize(view.getPreferredSize());
Rectangle viewRect = new Rectangle(0, 0, view.getWidth(), view
.getHeight());
int x;
int y;
Alignment alignX = ((CustomTransformUI) layerUI).getAlignX();
Alignment alignY = ((CustomTransformUI) layerUI).getAlignY();
if(alignX == Alignment.LEFT) {
x = (int) (innerArea.getX() - viewRect.getX());
} else if(alignX == Alignment.RIGHT) {
x = (int) (innerArea.getX()+innerArea.getWidth()-viewRect.getWidth()-viewRect.getX());
} else {
x = (int) Math.round(innerArea.getCenterX()
- viewRect.getCenterX());
}
if(alignY == Alignment.TOP) {
y = (int) (innerArea.getY() - viewRect.getY());
} else if(alignY == Alignment.BOTTOM) {
y = (int) (innerArea.getY()+innerArea.getHeight()-viewRect.getHeight()-viewRect.getY());
} else {
y = (int) Math.round(innerArea.getCenterY()
- viewRect.getCenterY());
}
viewRect.translate(x, y);
view.setBounds(viewRect);
}
if (glassPane != null) {
glassPane.setLocation(0, 0);
glassPane.setSize(layer.getWidth(), layer.getHeight());
}
return;
}
super.layoutContainer(parent);
}
In TransformUI :
private Alignment alignX; // horizontal alignment
private Alignment alignY; // verticalalignment
public TransformUI(TransformModel model, Alignment alignX, Alignment alignY) {
super();
this.setModel(model);
this.alignX = alignX;
this.alignY = alignY;
}
public Alignment getAlignX() {
return alignX;
}
public Alignment getAlignY() {
return alignY;
}
In TransformModel:
private Alignment alignX = Alignment.CENTER;
private Alignment alignY = Alignment.CENTER;
public CustomTransformModel(Alignment alignX, Alignment alignY) {
super();
this.alignX = alignX;
this.alignY = alignY;
}
#Override
public AffineTransform getTransform(JXLayer<? extends JComponent> layer) {
JComponent view = (JComponent)layer.getView();
/*
* Set the current actual program values in addition to the user
* options.
*/
this.setValue(Type.LayerWidth, layer == null ? 0 : layer.getWidth());
this.setValue(Type.LayerHeight, layer == null ? 0 : layer.getHeight());
this.setValue(Type.ViewWidth, view == null ? 0 : view.getWidth());
this.setValue(Type.ViewHeight, view == null ? 0 : view.getHeight());
/*
* If any change to previous values, recompute the transform.
*/
if (!Arrays.equals(this.prevValues, this.values)) {
System.arraycopy(this.values, 0, this.prevValues, 0, this.values.length);
this.transform.setToIdentity();
if (view != null) {
double scaleX;
double scaleY;
double centerX;
if(this.alignX == Alignment.LEFT) {
centerX = 0.0;
} else if (this.alignX == Alignment.RIGHT){
centerX = layer == null ? 0.0 : (double)layer.getWidth();
} else {
centerX = layer == null ? 0.0 : (double)layer.getWidth() / 2.0;
}
double centerY;
if(this.alignY == Alignment.TOP) {
centerY = 0.0;
} else if(this.alignY == Alignment.BOTTOM){
centerY = layer == null ? 0.0 : (double)layer.getHeight();
} else {
centerY = layer == null ? 0.0 : (double)layer.getHeight() / 2.0;
}
AffineTransform nonScaledTransform = this.transformNoScale(centerX, centerY);
if (((Boolean)this.getValue(Type.ScaleToPreferredSize)).booleanValue()) {
scaleY = scaleX = ((Double)this.getValue(Type.PreferredScale)).doubleValue();
} else {
Area area = new Area(new Rectangle2D.Double(0.0, 0.0, view.getWidth(), view.getHeight()));
area.transform(nonScaledTransform);
Rectangle2D bounds = area.getBounds2D();
scaleX = layer == null ? 0.0 : (double)layer.getWidth() / bounds.getWidth();
scaleY = layer == null ? 0.0 : (double)layer.getHeight() / bounds.getHeight();
if (((Boolean)this.getValue(Type.PreserveAspectRatio)).booleanValue()) {
scaleY = scaleX = Math.min(scaleX, scaleY);
}
}
this.transform.translate(centerX, centerY);
this.transform.scale((Boolean)this.getValue(Type.Mirror) != false ? - scaleX : scaleX, scaleY);
this.transform.translate(- centerX, - centerY);
this.transform.concatenate(nonScaledTransform);
}
}
return this.transform;
}
You can now create a zoomable panel with configurable alignment using:
TransformModel model = new TransformModel(Alignment.LEFT, Alignment.TOP);
TransformUI ui = new TransformUI(model, Alignment.LEFT, Alignment.TOP);
new JXLayer((Component)component, (LayerUI)ui)
Note that's a quick fix. It can probably be improved.

Painting an arbitrary collection of points quickly

I am writing a darts application, and have implemented a Dartboard which is painted as a BufferedImage.
When rendering the dartboard, I first iterate over the co-ordinates of the BufferedImage and calculate the 'segment' that it resides in. I wrap this up into a DartboardSegment, which is basically just a collection of points with a small amount of extra structure (what number on the board it corresponds to, etc).
Currently, to actually render the dartboard I paint each point individually, like the following:
for (Point pt : allPoints)
{
DartboardSegment segment = getSegmentForPoint(pt);
Color colour = DartboardUtil.getColourForSegment(segment);
int rgb = colour.getRGB();
int x = (int)pt.getX();
int y = (int)pt.getY();
dartboardImage.setRGB(x, y, rgb);
}
Obviously this takes some time. It's not an intolerable amount (~2-3s to paint a 500x500 area), but I'd like to eliminate this 'lag' if I can. In other areas of my application I have encountered alternate methods (such as Graphics.fillRect()) which are much faster.
I've seen that there is a fillPolgyon() method on the Graphics class, however I don't think I can easily convert my segments into polygons because their shapes vary (e.g. the shape of a triple, a circle for the bullseye...). Is there a faster way in java to paint an arbitrary array of Points at once, rather than looping through and painting individually?
The code that I want to write is something like:
for (DartboardSegment segment : allSegments)
{
Color colour = DartboardUtil.getColourForSegment(segment);
Polgyon poly = segment.toPolygon();
Graphics gfx = dartboardImage.getGraphics();
gfx.setColor(colour);
gfx.fillPolygon(poly);
}
I don't think I can easily convert my segments into polygons because their shapes vary (e.g. the shape of a triple, a circle for the bullseye...)
Here is something that may give you some ideas.
You can create Shape objects to represent each area of the dartboard:
import java.awt.*;
import java.util.*;
import javax.swing.*;
import java.awt.geom.*;
public class Dartboard extends JPanel
{
private ArrayList<DartboardSegment> segments = new ArrayList<DartboardSegment>();
private int size = 500;
private int radius = size / 2;
private int border = 25;
private int doubleSize = size - (2 * border);
private int tripleSize = size / 2;
private int thickness = 10;
public Dartboard()
{
createSegmentWedges();
int innerRadius = size - (2 * border);
Shape outer = new Ellipse2D.Double(0, 0, size, size);
Shape inner = new Ellipse2D.Double(border, border, innerRadius, innerRadius);
Area circle = new Area( outer );
circle.subtract( new Area(inner) );
segments.add( new DartboardSegment(circle, Color.BLACK) );
createBullsEye();
}
private void createSegmentWedges()
{
int angle = -99;
for (int i = 0; i < 20; i++)
{
// Create the wedge shape
GeneralPath path = new GeneralPath();
path.moveTo(250, 250);
double radians1 = Math.toRadians( angle );
double x1 = Math.cos(radians1) * radius;
double y1 = Math.sin(radians1) * radius;
path.lineTo(x1 + 250, y1 + 250);
angle += 18;
double radians2 = Math.toRadians( angle );
double x2 = Math.cos(radians2) * radius;
double y2 = Math.sin(radians2) * radius;
path.lineTo(x2 + 250, y2 + 250);
path.closePath();
Color wedgeColor = (i % 2 == 0) ? Color.BLACK : Color.WHITE;
segments.add( new DartboardSegment(path, wedgeColor) );
// Create the double/triple shapes
Color color = (i % 2 == 0) ? Color.RED : Color.GREEN;
createShape(doubleSize, path, color);
createShape(tripleSize, path, color);
}
}
private void createShape(int outerSize, GeneralPath path, Color color)
{
int outerOffset = (size - outerSize) / 2;
int innerSize = outerSize - (2 * thickness);
int innerOffset = (size - innerSize) / 2;
Shape outer = new Ellipse2D.Double(outerOffset, outerOffset, outerSize, outerSize);
Shape inner = new Ellipse2D.Double(innerOffset, innerOffset, innerSize, innerSize);
Area circle = new Area( outer );
circle.subtract( new Area(inner) );
circle.intersect( new Area(path) );
segments.add( new DartboardSegment(circle, color) );
}
private void createBullsEye()
{
int radius1 = 40;
int offset1 = (size - radius1) / 2;
Ellipse2D.Double bullsEye1 = new Ellipse2D.Double(offset1, offset1, radius1, radius1);
segments.add( new DartboardSegment(bullsEye1, Color.GREEN) );
int radius2 = 20;
int offset2 = (size - radius2) / 2;
Ellipse2D.Double bullsEye2 = new Ellipse2D.Double(offset2, offset2, radius2, radius2);
segments.add( new DartboardSegment(bullsEye2, Color.RED) );
}
#Override
protected void paintComponent(Graphics g)
{
super.paintComponent(g);
Graphics2D g2d = (Graphics2D)g.create();
for (DartboardSegment segment: segments)
{
g2d.setColor( segment.getColor() );
g2d.fill( segment.getShape() );
}
}
#Override
public Dimension getPreferredSize()
{
return new Dimension(500, 500);
}
class DartboardSegment
{
private Shape shape;
private Color color;
public DartboardSegment(Shape shape, Color color)
{
this.shape = shape;
this.color = color;
}
public Shape getShape()
{
return shape;
}
public Color getColor()
{
return color;
}
}
private static void createAndShowGUI()
{
JFrame frame = new JFrame("DartBoard");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new Dartboard());
frame.setLocationByPlatform( true );
frame.pack();
frame.setVisible( true );
}
public static void main(String[] args)
{
EventQueue.invokeLater( () -> createAndShowGUI() );
/*
EventQueue.invokeLater(new Runnable()
{
public void run()
{
createAndShowGUI();
}
});
*/
}
}
After a bit more digging, I think one solution to this is to do the following. It's not the neatest, but I think it will work:
int i = 0;
for (int y=0; y<height; y++)
{
for (int x=0; x<width; x++)
{
Point pt = new Point(x, y);
DartboardSegment segment = getSegmentForPoint(pt);
Color colour = DartboardUtil.getColourForSegment(segment);
pixels[i] = colorToUse.getRGB();
i++;
}
}
dartboardImage.setRGB(0, 0, width, height, pixels, 0, width);
I am open to better suggestions, however!

Why doesn't a FontMetrics' .getStringBounds() return the correct text width?

The code below uses batik-svggen to generate SVG files; the goal is an oh-so-classical goal, but seemingly impossible to get right the first time... Correctly calculate the height and width of some rendered text so that it can be surrounded by a shape -- here, a rectangle.
The class which is used is as follows:
public final class SvgParseNode
{
private static final int LEFTMARGIN = 5;
private static final int TOPMARGIN = 5;
private static final int RIGHTMARGIN = 5;
private static final int BOTTOMARGIN = 5;
private final Graphics2D graphics;
private final String text;
private final int xStart;
private final int yStart;
private final int rwidth;
private final int rheight;
private final int textXOffset;
private final int textYOffset;
#SuppressWarnings("ObjectToString")
public SvgParseNode(final Graphics2D graphics, final ParseNode node,
final int xStart, final int yStart)
{
this.xStart = xStart;
this.yStart = yStart;
this.graphics = Objects.requireNonNull(graphics);
text = Objects.requireNonNull(node).toString();
/*
* First, get the bounds of the text
*/
final Font font = graphics.getFont();
final FontMetrics metrics = graphics.getFontMetrics(font);
final Rectangle2D bounds = metrics.getStringBounds(text, graphics);
final int boundsHeight = (int) bounds.getHeight();
/*
* The width of the target rectangle is that of the bounds width, plus
* both left and right margin on the x axis
*/
// PROBLEM HERE
//rwidth = (int) bounds.getWidth() + LEFTMARGIN + RIGHTMARGIN;
rwidth = metrics.stringWidth(text) + LEFTMARGIN + RIGHTMARGIN;
/*
* The height is that of the bounds height, plus both up and down
* margins on the y avis
*/
rheight = boundsHeight + TOPMARGIN + BOTTOMARGIN;
/*
* The x offset of the text is that of the left x margin
*/
textXOffset = LEFTMARGIN;
/*
* The y offset is that of half of half the bounds height plus the y
* up margin
*/
textYOffset = rheight / 2 + TOPMARGIN;
}
public void render()
{
final Shape rect = new Rectangle(xStart, yStart, rwidth, rheight);
graphics.draw(rect);
graphics.drawString(text, xStart + textXOffset, yStart + textYOffset);
}
public Point getTopAttachPoint()
{
return new Point(xStart + rwidth / 2, yStart);
}
public Point getDownAttachPoint()
{
return new Point(xStart + rwidth / 2, yStart + rheight);
}
}
At the line marked PROBLEM HERE above, I tried to use the width given by .getStringBounds() as the width of the text. Fat luck, it doesn't return the correct result... But the .stringWidth() works.
I use this to write the rendering (code adapted from here):
public final class SvgWriting
{
private static final int XSTART = 10;
private static final int YSTART = 10;
private static final Path OUTFILE;
static {
final String s = System.getProperty("java.io.tmpdir");
if (s == null)
throw new ExceptionInInitializerError("java.io.tmpdir not defined");
OUTFILE = Paths.get(s, "t2.svg");
}
private SvgWriting()
{
throw new Error("no instantiation is permitted");
}
public static void main(final String... args)
throws IOException
{
// Get a DOMImplementation.
final DOMImplementation domImpl =
GenericDOMImplementation.getDOMImplementation();
// Create an instance of org.w3c.dom.Document.
final String svgNS = "http://www.w3.org/2000/svg";
final Document document = domImpl.createDocument(svgNS, "svg", null);
// Create an instance of the SVG Generator.
final SVGGraphics2D svgGenerator = new SVGGraphics2D(document);
final SvgParseNode node = new SvgParseNode(svgGenerator,
new DummyNode(), XSTART, YSTART);
node.render();
try (
final Writer out = Files.newBufferedWriter(OUTFILE);
) {
svgGenerator.stream(out, false);
}
}
private static final class DummyNode
extends ParseNode
{
private DummyNode()
{
super("foo", Collections.emptyList());
}
#Override
public String toString()
{
return "I suck at graphics, I said!";
}
}
}
What I have noticed is that the longer the text, the "more incorrect" .getStringBounds() seems to be; this makes me say that somehow it does not account for the space between each rendered glyph, but that is just a hypothesis.
In the example above, metrics.getStringBounds(...).getWidth() returns ~105, and metrics.stringWidth() return 111.
Anyway, it is really strange; why doesn't .getStringBounds() return a "correct" width? What am I missing?

AffineTransform seeming to ignore component bounds

I have the following:
public class ParametricEQView extends JPanel implements PluginView {
private static final int BAND_WIDTH = 3;
private static final int THROW_HEIGHT = 64;
private static final int WIDTH = 128*BAND_WIDTH + 2*MARGIN;
private static final int HEIGHT = 2*THROW_HEIGHT + 2*MARGIN;
private static final int MID_HEIGHT = THROW_HEIGHT + MARGIN;
private final ParametricEQ _peq;
public ParametricEQView(ParametricEQ peq) {
super();
_peq = peq;
SwingUtils.freezeSize(this, WIDTH, HEIGHT);
setToolTipText("Parametric Equalizer");
}
#Override
public void paint(Graphics g) {
final Graphics2D g2d = (Graphics2D) g;
final int max = findMax();
g.setColor(BACKGROUND);
g.fillRect(0, 0, WIDTH, HEIGHT);
g.setColor(DATA);
final double scalingFactor = -((double) THROW_HEIGHT) / max;
final double[] fineLevels = _peq.getFineLevels();
int x = MARGIN;
int h;
final int[] xPoints = new int[128];
final int[] yPoints = new int[128];
for (int i = 0; i < 128; ++i) {
h = (int) (fineLevels[i] * scalingFactor);
xPoints[i] = x;
yPoints[i] = MID_HEIGHT + h;
x += BAND_WIDTH;
}
g.drawPolyline(xPoints, yPoints, 128);
g.setColor(AXES);
g.drawLine(MARGIN, MARGIN, MARGIN, HEIGHT-MARGIN);
g.drawLine(MARGIN, MID_HEIGHT, WIDTH-MARGIN, MID_HEIGHT);
g.setFont(AXIS_FONT);
final FontMetrics metrics = g.getFontMetrics();
int width = (int) metrics.getStringBounds(AXIS_LABEL_INPUT_MIDINUM, g).getWidth();
g.drawString(AXIS_LABEL_INPUT_MIDINUM, WIDTH-MARGIN-width, HEIGHT-3);
final AffineTransform atx = new AffineTransform();
atx.setToRotation(-Math.PI/2, 0, HEIGHT);
g2d.setTransform(atx);
final String topLabel = "+" + max;
width = (int) metrics.getStringBounds(topLabel, g).getWidth();
g2d.drawString(topLabel, HEIGHT-MARGIN-width, HEIGHT+10);
width = (int) metrics.getStringBounds(AXIS_LABEL_OUTPUT_VELOCITY, g).getWidth();
g2d.drawString(AXIS_LABEL_OUTPUT_VELOCITY, MID_HEIGHT-(width/2), HEIGHT+10);
g2d.drawString("-" + max, MARGIN, HEIGHT+10);
}
private int findMax() {
int max = 3;
for (int i = 0; i < 128; ++i)
max = Math.max(max, (int) Math.ceil(Math.abs(_peq.getFineLevels()[i])));
return max;
}
}
This is what it looks like:
The ParametricEQView is the component with the white background filling most of the window. In this image its coordinates are (0,0) in the containing frame and everything is great. However, if I resize the window so that the ParametricEQView moves over a bit (it has a fixed size and is set to be centered in its available space), the rotated text stays relative to the (0,0) of the frame instead of the component:
Everything else draws relative to the panel, it's just the rotated text that doesn't. What am I doing wrong?
When you call g2d.setTransform(atx); you override the transform currently set in the Graphics object, i.e. the translation between the panel and its parent frame. That's why the text is drawn in the frame referential, and not in the panel referential.
The correct code would be to get the current transform and modify it or directly call Graphics2D.rotate(double).
1) For custom paintings you need to override protected void paintComponent(Graphics g) instead of public void paint(Graphics g). Read more about customPaintings.
2)Seems you have your problem, because you do something like next for creation of GUI:
JFrame frame = new JFrame();
JPanel p = new JPanel();
ParametricEQView view = new ParametricEQView();
view.setPreferredSize(new Dimension(200,200));
p.add(view);
frame.add(p);
in that case ParametricEQView doesn't resize as you want, because JPanel use FlowLayout as default.
You need to use another LayoutManager for your panel, for example BorderLayout.
Try something like next:
JFrame frame = new JFrame();
ParametricEQView view = new ParametricEQView();
frame.add(view);
or
JPanel panel = new JPanel(new BorderLayout());
panel.add(view,BorderLayout.CENTER);
frame.add(panel);
in that case your ParametricEQView panel will be painting in proper way.

Categories

Resources