0

I'm currently developing a Hidato Puzzle solver for a university project. I'm using Swing and Intellij IDE.

One of the requirements is to have Square, Hexagonal and Triangular shapes. Square shapes are easily, but to implement the HexagonalGrid i wrote a custom Hexagonal Button extending JButton.

Result of trying to render the Grid

However, when trying to render a Grid i get this result. I don't know what's wrong.

I got the Hexagonal Math from this website which is aparently regarded online as the HexGrid Bible.

Here's the code of the Hexagonal Button and the Grid renderer.

import javax.swing.*;
import java.awt.*;

public class HexagonButton extends JButton {
private Shape hexagon;
float size;
float width, height;

public HexagonButton(float centerX, float centerY, float size){
    Polygon p = new Polygon();
    this.size = size;
    p.reset();
    for(int i=0; i<6; i++){
        float angleDegrees = (60 * i) - 30;
        float angleRad = ((float)Math.PI / 180.0f) * angleDegrees;

        float x = centerX + (size * (float)Math.cos(angleRad));
        float y = centerY + (size * (float)Math.sin(angleRad));

        p.addPoint((int)x,(int)y);
    }

    width = (float)Math.sqrt(3) * size;
    height = 2.0f * size;
    hexagon = p;
}

public void paintBorder(Graphics g){
    ((Graphics2D)g).draw(hexagon);
}

public void paintComponent(Graphics g){
    ((Graphics2D)g).draw(hexagon);
}

@Override
public Dimension getPreferredSize(){

    return new Dimension((int)width, (int)height);
}
@Override
public Dimension getMinimumSize(){
    return new Dimension((int)width, (int)height);
}
@Override
public Dimension getMaximumSize(){
    return new Dimension((int)width, (int)height);
}


@Override
public boolean contains(int x, int y){
    return hexagon.contains(x,y);
}
}

Public class Main extends JFrame implements MouseListener {

/**
 * Create the GUI and show it.  For thread safety,
 * this method should be invoked from the
 * event-dispatching thread.
 */
public static void main(String[] args) {
// write your code here
    //Schedule a job for the event-dispatching thread:
    //creating and showing this application's GUI.
    Main main = new Main();
    main.pack();
    main.setVisible(true);
    main.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}

Main(){
    super();
    int rows = 3;
    int cols = 3;
    setLayout(new GridLayout(rows,cols,-1,-1));
    //grid.setBorder(BorderFactory.createEmptyBorder(2,2,2,2));
    this.setMinimumSize(new Dimension(173 * rows, 200 * cols+2));

    for(int i=0;i<rows;i++){
        for(int j=0;j<cols;j++){
            float size = 25;
            int width = (int)(size * Math.sqrt(3));
            int height = (int)(size * 2.0f);
            int xOffset = (width / 2);
            if(i%2==1){
                //Offset odd rows to the right
                xOffset += (width/2);
            }
            int yOffset = height / 2;
            int centerX = xOffset + j*width;
            int centerY = yOffset + i*height;
            HexagonButton hexagon = new HexagonButton(centerX, centerY, size);
            hexagon.addMouseListener(this);
            hexagon.setMinimumSize(hexagon.getMinimumSize());
            hexagon.setMaximumSize(hexagon.getMaximumSize());
            hexagon.setPreferredSize(hexagon.getPreferredSize());
            //hexagon.setVerticalAlignment(SwingConstants.CENTER);
            hexagon.setHorizontalAlignment(SwingConstants.CENTER);
            add(hexagon);
        }

    }

}
}

Does anyone know where the problem might be? I'm still pretty new to Swing

Aleix Sanchis
  • 302
  • 3
  • 12
  • Use a `GridBagLayout` instead. – MadProgrammer Jun 02 '18 at 23:42
  • 2
    I can't imagine that **any** layout will help. If the goal here is to have the hexagons merge visually into a honeycomb pattern, the rectangular `JButton`s on which the hexagons are drawn will have to overlap. The normal Swing layout managers naturally resist overlapping the components under their control. You might be able to write a **custom layout manager**, however... – Kevin Anderson Jun 02 '18 at 23:51
  • If you want to follow the path you are, pass ONLY the `size` to the buttons, let them calculate the rest of the their requirements. However, if your intention is to create a honeycomb pattern, I'd paint ALL the polygons onto a single component. Maintain their `Shape`s in a `List` so they can more easily be painted and you can perform collision detection on them more easily. – MadProgrammer Jun 02 '18 at 23:53

1 Answers1

3

For what it's worth, I wouldn't use custom components for each cell, rather, I'd use a single component, generate all the shapes you need and paint them onto a single component.

This gives you significantly more control over the placement of the shapes, and since Shape has built in collision detection, it makes it very easy to detect which cell the mouse is over/clicked.

I did some tweaking of your basic code and came up with the following example.

It creates a single shape which acts as the "master", I then, for each cell, create an Area and translate it's position to where I want it to appear. This is then added to a List which is used to paint all the cells and perform the mouse "hit" detection.

The benefit of this approach is, it automatically scales the shapes to fit within the available component space

Example

import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.GeneralPath;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class Test {

    public static void main(String[] args) {
        new Test();
    }

    public Test() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

//      private Shape hexagon;
        private List<Shape> cells = new ArrayList<>(6);

        private Shape highlighted;

        public TestPane() {
            addMouseMotionListener(new MouseAdapter() {
                @Override
                public void mouseMoved(MouseEvent e) {
                    highlighted = null;
                    for (Shape cell : cells) {
                        if (cell.contains(e.getPoint())) {
                            highlighted = cell;
                            break;
                        }
                    }
                    repaint();
                }
            });
        }

        @Override
        public void invalidate() {
            super.invalidate();
            updateHoneyComb();
        }

        protected void updateHoneyComb() {
            GeneralPath path = new GeneralPath();

            float rowHeight = ((getHeight() * 1.14f) / 3f);
            float colWidth = getWidth() / 3f;

            float size = Math.min(rowHeight, colWidth);

            float centerX = size / 2f;
            float centerY = size / 2f;
            for (float i = 0; i < 6; i++) {
                float angleDegrees = (60f * i) - 30f;
                float angleRad = ((float) Math.PI / 180.0f) * angleDegrees;

                float x = centerX + ((size / 2f) * (float) Math.cos(angleRad));
                float y = centerY + ((size / 2f) * (float) Math.sin(angleRad));

                if (i == 0) {
                    path.moveTo(x, y);
                } else {
                    path.lineTo(x, y);
                }
            }
            path.closePath();

            cells.clear();
            for (int row = 0; row < 3; row++) {
                float offset = size / 2f;
                int colCount = 2;
                if (row % 2 == 0) {
                    offset = 0;
                    colCount = 3;
                }
                for (int col = 0; col < colCount; col++) {
                    AffineTransform at = AffineTransform.getTranslateInstance(offset + (col * size), row * (size * 0.8f));
                    Area area = new Area(path);
                    area = area.createTransformedArea(at);
                    cells.add(area);
                }
            }

        }

        public void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();
            if (highlighted != null) {
                g2d.setColor(Color.BLUE);
                g2d.fill(highlighted);
            }
            g2d.setColor(Color.BLACK);
            for (Shape cell : cells) {
                g2d.draw(cell);
            }
            g2d.dispose();
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(200, 200);
        }

    }
}

Now, I know, you're going to tell me that there is space between them. Based on the fact that the first cell should be at 0x0, I'd say the space is coming from your hexagon algorithm, but I'll leave you to sort that out ;)

Also, the rows/columns are currently hard coded (3x3), shouldn't be to hard to make them more variable ;)

Update

So, I had a play around with the positioning algorithm and based on you linked hexagon algorithm, I was able to come up with...

    protected void updateHoneyComb() {
        GeneralPath path = new GeneralPath();

        double rowHeight = ((getHeight() * 1.14f) / 3f);
        double colWidth = getWidth() / 3f;

        double size = Math.min(rowHeight, colWidth) / 2d;

        double centerX = size / 2d;
        double centerY = size / 2d;

        double width = Math.sqrt(3d) * size;
        double height = size * 2;
        for (float i = 0; i < 6; i++) {
            float angleDegrees = (60f * i) - 30f;
            float angleRad = ((float) Math.PI / 180.0f) * angleDegrees;

            double x = centerX + (size * Math.cos(angleRad));
            double y = centerY + (size * Math.sin(angleRad));

            if (i == 0) {
                path.moveTo(x, y);
            } else {
                path.lineTo(x, y);
            }
        }
        path.closePath();

        cells.clear();
        double yPos = size / 2d;
        for (int row = 0; row < 3; row++) {
            double offset = (width / 2d);
            int colCount = 2;
            if (row % 2 == 0) {
                offset = 0;
                colCount = 3;
            }
            double xPos = offset;
            for (int col = 0; col < colCount; col++) {
                AffineTransform at = AffineTransform.getTranslateInstance(xPos + (size * 0.38), yPos);
                Area area = new Area(path);
                area = area.createTransformedArea(at);
                cells.add(area);
                xPos += width;
            }
            yPos += height * 0.75;
        }

    }

Which now gets the hexagons laid out against each other. Still needs some work though

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • Hi! Regarding this project, today i found myself facing another obstacle with a dynamic list. I found your answer https://stackoverflow.com/questions/14615888/list-of-jpanels-that-eventually-uses-a-scrollbar and i just wanted to say i love you and i will buy a beer if i ever go to Melbourne <3 – Aleix Sanchis Jun 08 '18 at 10:55