I'm creating a text field in java using swing components. I want to make a search text field like one appears in Mozilla or other browsers.
I have added a button in text field. I have set border layout of JTextField. everything is working fine but whenever large text is written in text field (as it reaches the given size of text field) it goes behind the button. As everyone of you must have seen, this does not occur in search bars. Text must not go behind the button rather there must be some gap between button and text.
Does anyone know how to do that?
Maybe start with something like this:
The blinking cursor is positioned at the far right of the text field.
import java.awt.*;
import java.awt.image.BufferedImage;
import javax.swing.*;
class ButtonsInTextField {
JPanel gui = new JPanel(new GridBagLayout());
JTextField textField;
ButtonsInTextField(int cols) {
JPanel textFieldWithButtonsPanel = new JPanel(new FlowLayout(
SwingConstants.LEADING, 5, 1));
textField = new JTextField(cols);
textFieldWithButtonsPanel.add(textField);
addButtonToPanel(textFieldWithButtonsPanel, 8);
addButtonToPanel(textFieldWithButtonsPanel, 16);
addButtonToPanel(textFieldWithButtonsPanel, 24);
// WARNING: Not sensitive to PLAF change!
textFieldWithButtonsPanel.setBackground(textField.getBackground());
textFieldWithButtonsPanel.setBorder(textField.getBorder());
textField.setBorder(null);
// END WARNING:
gui.add(textFieldWithButtonsPanel);
}
private final void addButtonToPanel(JPanel panel, int height) {
BufferedImage bi = new BufferedImage(
// find the size of an icon from the system,
// this is just a guess
24, height, BufferedImage.TYPE_INT_RGB);
JButton b = new JButton(new ImageIcon(bi));
b.setContentAreaFilled(false);
//b.setBorderPainted(false);
b.setMargin(new Insets(0,0,0,0));
panel.add(b);
}
public final JComponent getGui() {
return gui;
}
public final JTextField getField() {
return textField;
}
public static void main(String[] args) {
Runnable r = new Runnable() {
#Override
public void run() {
ButtonsInTextField bitf = new ButtonsInTextField(20);
JOptionPane.showMessageDialog(null, bitf.getGui());
}
};
// Swing GUIs should be created and updated on the EDT
// http://docs.oracle.com/javase/tutorial/uiswing/concurrency
SwingUtilities.invokeLater(r);
}
}
As people have noted above, it might have helped to see the code, especially the Layout manager.
However, you might try the following (if you haven't yet):
Call setColumns
http://docs.oracle.com/javase/7/docs/api/javax/swing/JTextField.html#setColumns(int)
Call setPreferredSize /setMaximumSize/setMinimumSize depending on your layout manager.
But I'd try to avoid this solution because it's pixel-level maintenance.
Regards
As an alternative solution you can use a Component Border, which allows you to use the button as a Border so it appears within the text field.
Related
I'm beginner in Java and I'm trying to make a simple text editor where the font and text size can be changed. It works although it has an issue that it seems that the text area size is fixed to the font and text size when something is changed.
I want the text area size all time be the same. I was debugging and I realize that rows and columns of text area never change.
Image before change font:
Image after change font:
Full code:
public class Run {
public static void main(String args[]) {
MainFrame main_frame = new MainFrame();
}
}
public class MainFrame extends JFrame{
private TextAreaPanel text_area_panel = new TextAreaPanel(60, 20);
private ComboBoxesPanel combo_boxes_panel = new ComboBoxesPanel(this.text_area_panel);
public MainFrame() {
this.initUI();
}
private void initUI() {
setLayout(new BorderLayout());
add(combo_boxes_panel, BorderLayout.NORTH);
add(text_area_panel, BorderLayout.CENTER);
setSize(getToolkit().getScreenSize().width / 2, getToolkit().getScreenSize().height / 2);
setLocation(getToolkit().getScreenSize().width / 4, getToolkit().getScreenSize().height / 4);
setTitle("Simple Text Editor");
setDefaultCloseOperation(EXIT_ON_CLOSE);
setVisible(true);
}
}
public class TextAreaPanel extends JPanel{
private JTextArea text_area = new JTextArea();
private JScrollPane scroll_text_area = new JScrollPane(text_area);
public TextAreaPanel(int width, int height) {
this.addComponets();
setTextAreaSize(width, height);
}
private void addComponets() {
add(scroll_text_area);
}
public void setTextAreaSize(int width, int height) {
text_area.setRows(height);
text_area.setColumns(width);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
//setTextAreaSize(getWidth() / 15, getHeight() / 20);
}
public JTextArea getArea() {
return this.text_area;
}
}
public class ComboBoxesPanel extends JPanel{
private String[] system_fonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames();
private Object[] sizes = {8,10,12,14,16,20,24,36, 48};
private JComboBox font_box = new JComboBox(system_fonts);
private JComboBox size_box = new JComboBox(sizes);
private TextAreaPanel text_area_panel;
public ComboBoxesPanel(TextAreaPanel text_area_panel) {
this.text_area_panel = text_area_panel;
font_box.addActionListener(font_listener);
size_box.addActionListener(size_listener);
add(font_box);
add(size_box);
}
private ActionListener font_listener = new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
(text_area_panel.getArea()).setFont(new Font((String)font_box.getSelectedItem(),
Font.PLAIN, (int)size_box.getSelectedItem()));
}
};
private ActionListener size_listener = new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
(text_area_panel.getArea()).setFont(new Font((String)font_box.getSelectedItem(),
Font.PLAIN, (int)size_box.getSelectedItem()));
}
};
}
Anyone knows how to solve this issue? Any idea how to solve this?
Also all the code in my GitLab repository if anyone wants to help.
https://gitlab.com/RichardCG/simpletexteditor
Thanks. :)
First of all I will rant a little about your variable names.
When you create a JTextArea you specify the "rows/columns" of the text area:
why do you use "width/height" as the variable names in your class? In Swing "width/height" are used to represent pixel values.
why did you change the order of the variables? You specify the "row, column" as the parameters of the text area. Why would you change your parameter order to "width, height". The "width" does NOT correlate to "rows".
It is confusing when the API is not consistent.
it seems that the text area size is fixed to the font and text size
Correct, all Swing components are responsible for determining their own preferred size. The size is based on the properties of the component. So when you change the font or font size the component will recalculate its preferred size.
This is a good thing as it allows:
the layout mangers to work properly
the scroll pane to work properly as the scrollbars will appear when needed.
I want the text area size all time be the same
You can control the size of the scroll pane, which in turn will keep the text area size the size while showing/hiding scrollbars as required.
In the constructor of you class you can add:
int scrollBarSize = UIManager.getInt("ScrollBar.width");
Dimension d = text_area.getPreferredSize();
d.width += scrollBarSize;
d.height += scrollBarSize;
scroll_text_area.setPreferredSize(d);
This works because the FlowLayout respects the preferred size of any component added to the panel.
However, this is NOT a good solution. Instead you should design your editor to be more user friendly.
For any editor I've used the editor component takes up all the available space in the frame. So you may have a tool bar (at the top), or status bar (at the bottom) and the editor takes up the remaining space. This allows the editor size to change as the user resizes the frame.
So typically you would just set the row/columns of the JTextArea when you create the text area and add the text area to a JScrollPane. Then the scroll pane is added to the CENTER of the BorderLayout on the. frame and you then pack() the frame BEFORE making the frame visible.
There is no need for the TextAreaPanel class.
Basically, I have added a JPanel to a JFrame, and whenever I press the button within the JPanel (which changes the visibility of a component within the JPanel), the size of the JPanel and the location of the components change. I cant for the life of me figure out why this problem is occurring. Any help is greatly appreciated.
Google Searches
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.*;
public class Main {
private JFrame window;
private JPanel loginPanel;
private JButton loginButton;
private JTextField usernameField;
private JPasswordField passwordField;
private JLabel usernameLabel, passwordLabel, errorMessage;
public static void main(String[] args) {
Main program = new Main();
}
public Main() {
createWindow();
runProgram();
}
private void createWindow() {
window = new JFrame("Dentist Program");
window.setSize(1450, 900);
window.setVisible(true);
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
loginPanel = new JPanel();
loginPanel.setSize(300, 400);
loginPanel.setBorder(BorderFactory.createLineBorder(Color.BLACK));
usernameLabel = new JLabel("Username");
usernameLabel.setSize(125, 20);
usernameLabel.setLocation(loginPanel.getWidth()/2 - usernameLabel.getWidth()/2, 100);
usernameField = new JTextField();
usernameField.setSize(125, 25);
usernameField.setLocation(loginPanel.getWidth()/2 - usernameField.getWidth()/2, usernameLabel.getY() + usernameLabel.getHeight());
passwordLabel = new JLabel("Password");
passwordLabel.setSize(125, 20);
passwordLabel.setLocation(loginPanel.getWidth()/2 - passwordLabel.getWidth()/2, usernameField.getY() + usernameField.getHeight() + 10);
passwordField = new JPasswordField();
passwordField.setSize(125, 25);
passwordField.setLocation(loginPanel.getWidth()/2 - passwordField.getWidth()/2, passwordLabel.getY() + passwordLabel.getHeight());
loginButton = new JButton("Log In");
loginButton.setSize(80, 25);
loginButton.setLocation(passwordField.getX(), passwordField.getY() + passwordField.getHeight() + 15);
loginButton.addActionListener(new ActionListener()
{
#Override
public void actionPerformed(ActionEvent arg0) {
errorMessage.setText("");
loginPanel.repaint();
}
});
errorMessage = new JLabel("Username and/or password is incorrect");
errorMessage.setSize(250, 25);
errorMessage.setLocation(loginPanel.getWidth()/2 - errorMessage.getWidth()/2, loginButton.getY() + loginButton.getHeight() + 25);
errorMessage.setForeground(Color.RED);
loginPanel.add(usernameLabel);
loginPanel.add(usernameField);
loginPanel.add(passwordLabel);
loginPanel.add(passwordField);
loginPanel.add(loginButton);
loginPanel.add(errorMessage);
window.getContentPane().add(loginPanel);
window.repaint();
}
private void runProgram() {
}
}
The reason your UI changes is because the look that you think is correct (the initial look) is actually wrong, and the resized version is the "correct" version.
Typically, you'd call pack() after adding components to a container, which tries to display the contents of that container at their preferred sizes. To see the correct UI before and after the button press, following your call to repaint(), add:
window.pack();
You may think that looks terrible (which it does) but it's exactly what your code is asking for, and is therefore the "correct" version.
Take note that you are trying to set sizes with explicit calls to setSize(), which is a bad practice in Swing. At most, you should be setting preferred sizes and letting the layout manager handle the details. When you try to micromanage component sizes and locations, you can often make it look good on one platform, but irredeemably bad on others. Remember that Java is supposed to be the "write once, run anywhere" language.
When you want more control over your layout, you should explicitly specify and use a layout manager, rather than just accepting the default layout. In almost every non-trivial UI, GridBagLayout is your best bet.
Finally, you don't need to explicitly call repaint() on your UI when something changes. Typically, you'd call paint() and repaint() when you are working with graphics. Let Swing handle the painting automatically.
I can't get the standard BorderFactory.createEmptyBorder working properly in Java Swing.
I tried it with a simple Java app that creates a Spinner View and declares an empty border around it. The border is shown everywhere except for the right side of the spinner view. but that only happened in Windows, on OSX it works as intended.
To clarify the use of a border: In my real application I have a visible border at the outside of the spinner and then I want to center the text inside. I found the createCompoundBorder property very useful for that.
import javax.swing.*;
import java.awt.*;
import javax.swing.border.Border;
class HelloWorldSwing {
public static void main(String[] args) {
try {
UIManager.setLookAndFeel(
UIManager.getSystemLookAndFeelClassName());
} catch (Exception e) {}
Runnable guiCreator = new Runnable() {
public void run() {
JFrame fenster = new JFrame("Hallo Welt mit Swing");
fenster.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
SpinnerNumberModel model = new SpinnerNumberModel(1,1,9,1);
JSpinner spinner = new JSpinner(model);
JFormattedTextField textField = ((JSpinner.DefaultEditor)spinner.getEditor()).getTextField();
textField.setOpaque(true);
// textField.setBackground(Color.);
spinner.setBorder(BorderFactory.createEmptyBorder(50, 100, 50, 100));
spinner.setUI(new javax.swing.plaf.basic.BasicSpinnerUI() {
protected Component createNextButton() {
Component c = new JButton();
c.setPreferredSize(new Dimension(0, 0));
c.setFocusable(false);
return c;
}
protected Component createPreviousButton() {
Component c = new JButton();
c.setPreferredSize(new Dimension(0, 0));
c.setFocusable(false);
return c;
}
});
spinner.setBackground(Color.LIGHT_GRAY);
fenster.add(spinner);
fenster.setSize(300, 200);
fenster.setVisible(true);
}
};
SwingUtilities.invokeLater(guiCreator);
}
}
=
I don't want to find a solution that implements one more UI element. This would be an easy and simple solution (Thanks to Andrew Thompson). The reason is that it is just a small project where I got into this error. In small projects I mostly want a clean and good looking code, which means that such bugs are fixed by trying to fix the broken code and not by doing a workaround.
It looks like a JSpinner is using a custom layout manager and is not handling the Border correctly.
I modified your code to give the buttons the width of the right edge of the border minus 1. I guess the layout manager leaves a 1 pixel gap between the edge of the text field and the button.
Seems to work on Windows, but it might mess up OSX?
import javax.swing.*;
import java.awt.*;
import javax.swing.border.Border;
class HelloWorldSwing {
public static void main(String[] args) {
try {
UIManager.setLookAndFeel(
UIManager.getSystemLookAndFeelClassName());
} catch (Exception e) {}
Runnable guiCreator = new Runnable() {
public void run() {
JFrame fenster = new JFrame("Hallo Welt mit Swing");
fenster.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
SpinnerNumberModel model = new SpinnerNumberModel(1,1,9,1);
JSpinner spinner = new JSpinner(model);
JFormattedTextField textField = ((JSpinner.DefaultEditor)spinner.getEditor()).getTextField();
textField.setOpaque(true);
// textField.setBackground(Color.);
spinner.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 0));
spinner.setUI(new javax.swing.plaf.basic.BasicSpinnerUI() {
protected Component createNextButton() {
Component c = new JButton();
c.setPreferredSize(new Dimension(9, 0));
c.setVisible(false);
return c;
}
protected Component createPreviousButton() {
Component c = new JButton();
c.setPreferredSize(new Dimension(9, 0));
c.setVisible(false);
return c;
}
});
spinner.setBackground(Color.LIGHT_GRAY);
fenster.setLayout( new FlowLayout() );
fenster.add(spinner);
fenster.setSize(300, 200);
fenster.setVisible(true);
}
};
SwingUtilities.invokeLater(guiCreator);
}
}
The other solution is to not use a JSpinner. You could easily use a JTextField and write a custom Action for the Up/Down arrows. A little more work but it will work on all platforms.
Also since you seems to be worried about creating extra components this will be far more efficient in that regard. The JSpinner is a complex component itself and the JFormattedTextField is far more complex than a simple JTextField.
I'm trying to add this JTextArea with a JScrollPane (with vertical but not horizontal scrollbar) to my frame but the result is just a grey area with a scrollbar on the right... I'm probably doing something really dumb but i've done that same exact thing to a JPanel and it worked
public class Chats {
public static int height = 600;
public static int length = 400;
public static void init(String me, String you){
JFrame frame = new JFrame ("Chat");
frame.setSize(larguraframe, alturaframe);
frame.setLocation(620, 100);
frame.setResizable(false);
frame.setLayout(null);
frame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
JTextArea chat = new JTextArea();
chat.setColumns(10);
chat.setLineWrap(true);
chat.setWrapStyleWord(true);
JScrollPane scrollpane = new JScrollPane(chat, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER;
scrollpane.setBounds(length/8 - 27, height/9 + 27, 350, 380);
chat.setPreferredSize(new Dimension(lenght-15, 7*height/8-27));
frame.add(chat);
frame.add(scrollpane);
frame.setVisible(true);
}
}
I don't mind changing my frame's Layout but i really want one that allows me to put stuff exactly where i want it. Thanks
EDIT
Okay it now shows on my frame but the TextArea is still not resizable. When i write something in it using a JTextfield and a JButton with a Listener that appends the JTextfield's text to the JTextArea and then sets the text in the JTextField to "" it only accepts up to a certain ammount of lines. After that it just looks the same.
I know that you've already "accepted" an answer, but I feel that I'd be remiss if I didn't give an answer that gave important points, ones that in the long run would allow you to create a better and more robust (i.e., a more easily debuggable, modifiable, and enhanceable) application.
Again,
Never set a JTextArea's preferredSize, as this will create a JTextArea whose size is inflexibly set that will not add additional lines when needed. Instead set the JTextArea's row and column properties.
While null layouts and setBounds() might seem to Swing newbies like the easiest and best way to create complex GUI's, the more Swing GUI'S you create the more serious difficulties you will run into when using them. They won't resize your components when the GUI resizes, they are a royal witch to enhance or maintain, they fail completely when placed in scrollpanes, they look gawd-awful when viewed on all platforms or screen resolutions that are different from the original one.
Better to use a JPanel, or more often multiple nested JPanels, each using its own layout manager that allow you to more simply create agile and powerful complex yet beautiful GUI's.
When using layout managers, you'll want to pack() your JFrame after adding all components so that all layout managers will do their jobs and layout components appropriately.
I've an example program below I show,
a title JLabel with large centered text
a JTextArea, called chatViewArea, of specified row and column size held within a JScrollPane and that is for viewing the chat. It is non-focusable so that the user cannot directly interact with it.
Another JTextArea, called textEntryArea, for entering text. You could use a simple JTextField for this and give it an ActionListener so that it responds to the enter key, however if you want a multi-lined text component that acts similar, you'll need to change the key bindings for the JTextArea's enter key. I've done that here so that the enter key "submits" the text held within the textEntryArea JTextArea, and the control-enter key combination acts the same as the enter key used to -- creating a new line.
The main JPanel uses simply a BorderLayout to allow placement of the title at the top, the chat view JTextArea in the center and the text entry JTextArea at the bottom. Note that if you needed to see more components, such as a JList showing other chatters, this can be done by nesting JPanels and using more layouts if need be.
For example:
import java.awt.BorderLayout;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import javax.swing.*;
#SuppressWarnings("serial")
public class Chat2 extends JPanel {
private static final int ROWS = 25; // rows in the chat view JTextArea
private static final int COLS = 40; // columns in the chat view JTextArea
// and text entry area
private static final int ENTRY_ROWS = 4; // rows in the text entry JTextArea
private static final int BL_HGAP = 10; // horizontal gap for our
// BorderLayout
private static final int BL_VGAP = 5; // vertical gap for our BorderLayout
private static final int EB_GAP = 15; // gap for empty border that goes
// around entire app
private static final String TITLE_TEXT = "My Chat Application";
private static final float TITLE_POINTS = 32f; // size of the title jlabel
// text
private JTextArea chatViewArea = new JTextArea(ROWS, COLS);
private JTextArea textEntryArea = new JTextArea(ENTRY_ROWS, COLS);
public Chat2() {
// label to display the title in bold large text
JLabel titleLabel = new JLabel(TITLE_TEXT, SwingConstants.CENTER);
titleLabel.setFont(titleLabel.getFont().deriveFont(Font.BOLD, TITLE_POINTS));
// set up the chat view JTextArea to have word wrap
// and to not be focusable
chatViewArea.setWrapStyleWord(true);
chatViewArea.setLineWrap(true);
chatViewArea.setFocusable(false);
// add it to a JScrollPane, and give the scrollpane vertical scrollbars
JScrollPane viewScrollPane = new JScrollPane(chatViewArea);
viewScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
// set up the text entry JTextArea
textEntryArea.setWrapStyleWord(true);
textEntryArea.setLineWrap(true);
// key bindings so that control-enter will act as enter and the enter key will "submit"
// the user input to the chat window and the chat server
// will allow us to use a multilined text entry area if desired instead
// of a single lined JTextField
setEnterKeyBinding(textEntryArea);
JScrollPane entryScrollPane = new JScrollPane(textEntryArea);
entryScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
// add an empty border around entire application
setBorder(BorderFactory.createEmptyBorder(EB_GAP, EB_GAP, EB_GAP, EB_GAP));
// make the main layout a BorderLayout
setLayout(new BorderLayout(BL_HGAP, BL_VGAP));
// add our components to the GUI
add(titleLabel, BorderLayout.PAGE_START);
add(viewScrollPane, BorderLayout.CENTER);
add(entryScrollPane, BorderLayout.PAGE_END);
}
// Again, use key bindings so that control-enter JTextArea will act as enter key
// and the enter key will "submit" the user input to the chat window and the chat server.
// When ctrl-enter is pushed the Action originally bound to the enter key will be called
// and when enter is pushed a new Action, the EnterKeyAction, will be called
private void setEnterKeyBinding(JTextArea textArea) {
int condition = JComponent.WHEN_FOCUSED; // only for focused entry key
InputMap inputMap = textArea.getInputMap(condition);
ActionMap actionMap = textArea.getActionMap();
KeyStroke entryKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0);
KeyStroke ctrlEntryKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.CTRL_DOWN_MASK);
// first give ctrl-enter the action held by enter
Object entryKey = inputMap.get(entryKeyStroke);
Action entryAction = actionMap.get(entryKey);
inputMap.put(ctrlEntryKeyStroke, ctrlEntryKeyStroke.toString());
actionMap.put(ctrlEntryKeyStroke.toString(), entryAction);
// now give enter key a new Action
EnterKeyAction enterKeyAction = new EnterKeyAction();
inputMap.put(entryKeyStroke, entryKeyStroke.toString());
actionMap.put(entryKeyStroke.toString(), enterKeyAction);
}
public void appendToChatArea(final String text) {
if (SwingUtilities.isEventDispatchThread()) {
chatViewArea.append(text + "\n");
} else {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
chatViewArea.append(text + "\n");
}
});
}
}
private class EnterKeyAction extends AbstractAction {
#Override
public void actionPerformed(ActionEvent e) {
String text = textEntryArea.getText();
textEntryArea.setText("");
chatViewArea.append("User: " + text + "\n");
// TODO send text to the chat server!
}
}
private static void createAndShowGui() {
Chat2 mainPanel = new Chat2();
JFrame frame = new JFrame("My Chat Window");
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.getContentPane().add(mainPanel);
// pack the JFrame so that it will size itself to its components
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGui();
}
});
}
}
Try setting a layout like this:
frame.setLayout(new BorderLayout());
And adding the scrollpane to the center:
frame.add(scrollpane, BorderLayout.CENTER);
Also remove the line pointed by Jire in his answer.
You don't need to add chat because it is adapted by scrollPane.
Remove this line: frame.add(chat);
Add
chat.setBounds(length/8 - 27, height/9 + 27, 330, 360);
And see the magic happen... but do tweek the arguments here in order to get the right dimensions.
For resizable frame just do frame.setResizable(true); instead of frame.setResizable(false);
I'm working on an application in Java which, among other things, is supposed to display the details of a Magic: The Gathering card in a text field (since I'm using Swing, this is currently a JTextPane).
Those details contain text and small icons, with some of those icons inlined with the text (so the text flows around them), and some icons, by my design, right-aligned with some left-aligned text in the same line.
I took an image from another application which uses a very similar design to the one I'm working on (although not in Java):
This is how it basically should look like.
Now, for the love of everything, I can't get that to work in a JTextPane.
I started with trying to do this with CSS, but found out that the JEditorPane and subclasses don't support the "float" attribute, so I tried it with using the Pane's StyledDocument instead.
I didn't get the first part to work (the icon and right-align text at the top always ignored their alignment and were placed directly after the left-aligned text in the line) at first, until I found this question.
It was suggested to use the following line of code:
pane.setEditorKit(new HTMLEditorKit());
which somehow indeed fixed my first issue.
But now I'm stuck at the second part, getting those icons in the second part inline with the text. Here's what I currently got:
What I've found is that for some reason, when you switch the JTextPane to html mode with an editor kit with the line of code above, inserting components just goes completely crazy.
Both the icons on the top (which I have merged into a single image actually), and the ones in the text below (not merged) are both inside of JLabels, but it doesn't matter if I add them as images or inside of JLabels. The images or labels are definitely not bigger than what you see there, I have no idea at all where the extra whitespace is coming from.
I found this question, and the answer suggest that this is some kind of bug or just weird behavior with the html mode of the JEditorPane.
If I remove the above code line again, I end up with my original problem:
Depending on where exactly the icons are in the text, I get all kinds of different weird results. I put together some more example pictures below:
So, how could I possibly fix this? The JTextPane is working fine for me, except for this part, but I could possibly use some other solution, as long as the end result still looks the same. Remember that I might want to add some other components (like a Button) in there, so I'd like to stick to something native to Swing if possible at all.
The user will not be able to edit the TextPane's contents, but I'd like to add an option later to copy all of the content in one click (so I'd rather stay with a text area).
Below, I have put together a (not really that minimal) working example for you to play around with:
(EDIT: Updated code at the bottom now! The old code is still there under the following link.)
http://pastebin.com/RwAdPCzb
The icons that I'm using are below. You'd need to rename them and change the path in the code.
Some things to notice:
In the beginning I styled the text using the `Style` class, as described in the "How to use Editor Panes and Text Panes" tutorial on the Oracle website (TextSamplerDemo.java, for your reference). With this, I wasn't even able to do the right-align part at the top. Strangely, when I used the `SimpleAttributeSet` class for styling instead, even with the very same settings, it works.
I tried different alignment options for both the text and the labels which contain the icons. Regardless of what options I used, there was no visible difference at all.
UPDATE 1:
After Sharcoux' answer, I have edited my code to have 2 JLabels above the actual JTextPane which hold the two lines that were supposed to have different alignings (a left- and a right-aligned part).
The JTextPane doesn't use a HTMLEditorKit anymore now, and I use insertIcon() to insert the icons into the text.
This way, the icons are inserted (almost) correctly!
Image here:
However, there are two small things that I'm still not satisfied with:
First:
I need to put everything into a JScrollPane because the text in the TextPane is much longer in my actual application. Since I now have three components instead of just the TextPane, I needed to put everything into a JPanel and that into the ScrollPane.
However, if you do it like this, the JTextPane doesn't know that its with should not exceed the JScrollPane's anymore. It stopps wrapping text and just grows as big as the entire text.
I have opened a new question for this, since I feel that this is a basic issue of Swing and deserves its own question. If you want to help, here is the link:
JTextComponent inside JPanel inside JScrollPane
Second:
This is probably not possible, but I guess I'll ask anyway. The icons have the same baseline as the text when you add them this way. Is they any way to move them just a bit lower? 2-3 pixels, maybe? They would align much better with the text that way. Two pictures below.
This is how it looks now:
And this is how I would like it to look:
Maybe I can subclass and override some part of the JTextPane to move all icons that are rendered on it down a set pixel amount, or something like that?
For reference, here is also my new, updated code. I replaced the old one above with a pastebin.com link, if you still want to look at it.
UPDATE 2:
My first problem has already been eliminated! I updated the code below to reflect that, too.
My second question still stands!
Here's the new code:
import java.awt.EventQueue;
import java.awt.Graphics2D;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.border.EmptyBorder;
import javax.swing.text.BadLocationException;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
import java.awt.GridLayout;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.JScrollPane;
import javax.swing.Scrollable;
import javax.swing.JTextPane;
import javax.swing.JViewport;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Color;
import java.awt.Container;
import java.awt.Font;
import javax.swing.ScrollPaneConstants;
public class MinimalExample extends JFrame {
private JPanel contentPane;
private JScrollPane scrollPane;
private JTextPane textPane;
// Setup some data for an example card:
String name = "Absorb Vis";
String set = "CON";
String manaCost = "{6}{B}";
String printedType = "Sorcery";
String artist = "Brandon Kitkouski";
String rulesText = "Target player loses 4 life and you gain 4 life.\n"
+ "Basic landcycling {1}{B} ({1}{B}, Discard this card: "
+ "Search your library for a basic land card, reveal it, and put "
+ "it into your hand. Then shuffle your library.)";
HashMap<String, BufferedImage> manaSymbolImages;
private ScrollablePanel textPanel;
//private JPanel textPanel;
private JPanel headlinesPanel;
private JPanel firstHeadlinePanel;
private JPanel secondHeadlinePanel;
private JLabel titleLabel;
private JLabel manaCostLabel;
private JLabel typeLabel;
private JLabel setLabel;
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
try {
MinimalExample frame = new MinimalExample();
frame.setVisible(true);
}
catch (Exception e) {
e.printStackTrace();
}
}
});
}
public MinimalExample() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setBounds(100, 100, 230, 400);
contentPane = new JPanel();
contentPane.setBackground(Color.WHITE);
contentPane.setBorder(null);
setContentPane(contentPane);
/* HTMLEditorKit eKit = new HTMLEditorKit();
* textPane.setEditorKit(eKit); HTMLDocument htmlDoc = (HTMLDocument)
* textPane.getDocument(); htmlDoc.setPreservesUnknownTags(false); */
contentPane.setLayout(new GridLayout(0, 1, 0, 0));
textPanel = new ScrollablePanel();
//textPanel = new JPanel();
textPanel.setBackground(Color.WHITE);
textPanel.setLayout(new BorderLayout(0, 0));
headlinesPanel = new JPanel();
headlinesPanel.setBorder(new EmptyBorder(2, 5, 3, 5));
headlinesPanel.setBackground(Color.WHITE);
textPanel.add(headlinesPanel, BorderLayout.NORTH);
headlinesPanel.setLayout(new GridLayout(0, 1, 0, 0));
firstHeadlinePanel = new JPanel();
firstHeadlinePanel.setBorder(new EmptyBorder(0, 0, 3, 0));
firstHeadlinePanel.setOpaque(false);
headlinesPanel.add(firstHeadlinePanel);
firstHeadlinePanel.setLayout(new BorderLayout(0, 0));
titleLabel = new JLabel("");
titleLabel.setFont(new Font("Tahoma", Font.BOLD, 12));
firstHeadlinePanel.add(titleLabel, BorderLayout.WEST);
manaCostLabel = new JLabel("");
firstHeadlinePanel.add(manaCostLabel, BorderLayout.EAST);
secondHeadlinePanel = new JPanel();
secondHeadlinePanel.setBorder(null);
secondHeadlinePanel.setOpaque(false);
headlinesPanel.add(secondHeadlinePanel);
secondHeadlinePanel.setLayout(new BorderLayout(0, 0));
typeLabel = new JLabel("");
typeLabel.setFont(new Font("Tahoma", Font.PLAIN, 12));
secondHeadlinePanel.add(typeLabel, BorderLayout.WEST);
setLabel = new JLabel("");
setLabel.setFont(new Font("Tahoma", Font.BOLD, 12));
secondHeadlinePanel.add(setLabel, BorderLayout.EAST);
scrollPane = new JScrollPane();
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
scrollPane.setBackground(Color.WHITE);
contentPane.add(scrollPane);
textPane = new JTextPane();
textPane.setBorder(new EmptyBorder(0, 3, 0, 3));
textPane.setAlignmentY(0.3f);
textPane.setEditable(false);
textPanel.add(textPane, BorderLayout.CENTER);
scrollPane.setViewportView(textPanel);
loadManaCostIcons();
setPaneText();
}
// This part inserts the text into the document of the text pane.
public void setPaneText() {
titleLabel.setText(name);
manaCostLabel.setIcon(combineSymbols(manaCost));
typeLabel.setText(printedType);
setLabel.setText(set);
StyledDocument textPaneDoc = textPane.getStyledDocument();
SimpleAttributeSet defaultAtts = new SimpleAttributeSet();
StyleConstants.setFontFamily(defaultAtts, "SansSerif");
StyleConstants.setFontSize(defaultAtts, 12);
SimpleAttributeSet rulesAtts = new SimpleAttributeSet(defaultAtts);
SimpleAttributeSet artistAtts = new SimpleAttributeSet(defaultAtts);
StyleConstants.setFontSize(artistAtts, 10);
addTextWithSymbols(rulesText, rulesAtts);
try {
textPaneDoc.insertString(textPaneDoc.getLength(), artist, artistAtts);
}
catch (BadLocationException e) {
e.printStackTrace();
}
textPane.revalidate();
textPane.repaint();
}
/* This adds the rest of the text to the pane. The codes for the symbols get
* replaced by the actual symbols and the text gets inserted piece by piece. */
public void addTextWithSymbols(String text, SimpleAttributeSet style) {
StyledDocument textPaneDoc = textPane.getStyledDocument();
Pattern symbolPattern = Pattern.compile("\\{(.*?)\\}");
try {
Matcher symbolMatcher = symbolPattern.matcher(text);
int previousMatch = 0;
while (symbolMatcher.find()) {
int start = symbolMatcher.start();
int end = symbolMatcher.end();
String subStringText = text.substring(previousMatch, start);
String currentMatch = text.substring(start, end);
if (subStringText.isEmpty() == false) {
textPaneDoc.insertString(textPaneDoc.getLength(), subStringText, style);
}
ImageIcon currentIcon = new ImageIcon(manaSymbolImages.get(currentMatch));
SimpleAttributeSet iconAtts = new SimpleAttributeSet();
JLabel iconLabel = new JLabel(currentIcon);
StyleConstants.setComponent(iconAtts, iconLabel);
textPane.insertIcon(currentIcon);
previousMatch = end;
}
String subStringText = text.substring(previousMatch);
if (subStringText.isEmpty() == false) {
textPaneDoc.insertString(textPaneDoc.getLength(), subStringText + "\n", style);
}
}
catch (Exception e) {
e.printStackTrace();
}
}
/* Everything below is more or less irrelevant. However, you might need to
* adjust the image image file paths. */
public void loadManaCostIcons() {
manaSymbolImages = new HashMap<String, BufferedImage>();
try {
// Most likely, those paths won't work for you!
File bFile = new File("resource/B.png");
File c1File = new File("resource/1.png");
File c6File = new File("resource/6.png");
manaSymbolImages.put("{B}", ImageIO.read(bFile));
manaSymbolImages.put("{1}", ImageIO.read(c1File));
manaSymbolImages.put("{6}", ImageIO.read(c6File));
}
catch (IOException e) {
e.printStackTrace();
}
}
public ImageIcon combineSymbols(String symbols) {
String[] manaSymbols = symbols.split("(?<=})");
int combinedWidth = 0;
int maxHeight = 0;
for (int i = 0; i < manaSymbols.length; i++) {
BufferedImage currentSymbolImage = manaSymbolImages.get(manaSymbols[i]);
combinedWidth += currentSymbolImage.getWidth();
if (maxHeight < currentSymbolImage.getWidth()) {
maxHeight = currentSymbolImage.getWidth();
}
}
BufferedImage combinedManaCostImage = new BufferedImage(combinedWidth, maxHeight,
BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = combinedManaCostImage.createGraphics();
int currentPosition = 0;
for (int i = 0; i < manaSymbols.length; i++) {
BufferedImage tempCurrentImage = manaSymbolImages.get(manaSymbols[i].trim());
graphics.drawImage(tempCurrentImage, null, currentPosition, 0);
currentPosition += tempCurrentImage.getWidth();
}
graphics.dispose();
return (new ImageIcon(combinedManaCostImage));
}
/* Original source of this is here:
* https://stackoverflow.com/questions/15783014/jtextarea-on-jpanel-inside-jscrollpane-does-not-resize-properly
* And one update to it is here:
* */
private static class ScrollablePanel extends JPanel implements Scrollable {
#Override
public Dimension getPreferredScrollableViewportSize() {
return super.getPreferredSize();
}
#Override
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation,
int direction) {
return 16;
}
#Override
public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation,
int direction) {
return 16;
}
#Override
public boolean getScrollableTracksViewportWidth() {
return true;
}
#Override
public boolean getScrollableTracksViewportHeight() {
boolean track = true;
Container parent = getParent();
if (parent instanceof JViewport) {
JViewport viewport = (JViewport) parent;
if (viewport.getHeight() < getPreferredSize().height) {
track = false;
}
}
return track;
}
}
}
I think that the issue is the way you insert your images, and most probably, from your combineSymbol method.
Here is the way to insert stuff in the JTextPane :
public class Test {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
JFrame mainFrame = new JFrame("test");
mainFrame.setSize(300, 100);
mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container pane = mainFrame.getContentPane();
pane.setLayout(new BorderLayout());
JTP jtp = new JTP();
pane.add(jtp);
mainFrame.setVisible(true);
}
});
}
static class JTP extends JTextPane {
JTP() {
HTMLEditorKit eKit = new HTMLEditorKit();
setEditorKit(eKit);
HTMLDocument htmlDoc = (HTMLDocument) getDocument();//the HTMLEditorKit automatically created an HTMLDocument
htmlDoc.setPreservesUnknownTags(false);//I advice you to put this line if you plan to insert some foreign HTML
//inserting plain text (just change null for an attributeSet for styled text)
try {
htmlDoc.insertString(0, "test", null);
} catch (BadLocationException ex) {
Logger.getLogger(Test.class.getName()).log(Level.SEVERE, null, ex);
}
//inserting images
insertIcon(new ImageIcon("image.png"));
insertIcon(new ImageIcon("image.png"));
//inserting components (With component, you should specify the yAlignment by yourself)
JLabel label = new JLabel(new ImageIcon("image.png"));
label.setAlignmentY(JLabel.TOP);
insertComponent(label);
}
}
}
But to make things easier, I strongly advice you to use a title line outside the JTextPane. Text editors aren't really made for text having different alignment on the same line. Here is what I would suggest :
public class Test {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
JFrame mainFrame = new JFrame("test");
mainFrame.setSize(300, 100);
mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container pane = mainFrame.getContentPane();
pane.setLayout(new BorderLayout());
pane.setBackground(Color.WHITE);
pane.add(new JTP());
pane.add(new Title(), BorderLayout.NORTH);
mainFrame.setVisible(true);
}
});
}
static class JTP extends JTextPane {
JTP() {
setEditable(false);
setOpaque(false);
HTMLEditorKit eKit = new HTMLEditorKit();
setEditorKit(eKit);
HTMLDocument htmlDoc = (HTMLDocument) getDocument();//the HTMLEditorKit automatically created an HTMLDocument
htmlDoc.setPreservesUnknownTags(false);//I advice you to put this line if you plan to insert some foreign HTML
//inserting plain text (just change null for an attributeSet for styled text)
try {
htmlDoc.insertString(0, "capacity : ", null);
} catch (BadLocationException ex) {
Logger.getLogger(Test.class.getName()).log(Level.SEVERE, null, ex);
}
//inserting images
insertIcon(new ImageIcon("image.png"));
insertIcon(new ImageIcon("image.png"));
//inserting components (With component, you should specify the yAlignment by yourself)
JLabel label = new JLabel(new ImageIcon("image.png"));
label.setAlignmentY(JLabel.TOP);
insertComponent(label);
}
}
static class Title extends JPanel {
Title() {
setLayout(new BorderLayout());
setOpaque(false);
add(new JLabel("<html><b>Card title</b></html>"), BorderLayout.CENTER);
add(new JLabel(new ImageIcon("image.png")), BorderLayout.EAST);
}
}
}
You can try to define your own TabStops to align icons.
If you know size of icon and width of JTextPane just add your content as
"Person Name -tab- icon" and set custom TabSet for the paragraph. The TabSet has just one TabStop. The TabStop position = jTextPaneWidth - iconWidth