Line Numbers in Java Swing

Articles —> Line Numbers in Java Swing

Drawing line numbers within a JComponent can often be a useful feature, be it for line number in a text component such as a JTextArea, or drawing the row numbers of a JTable. An easy method one may come up with is to modify the model - for instance change the text of a JTextArea or add a column in a JTable to show line numbers - there are ample demonstrations of accomplishing line numbers by affecting the model (see How To: Add Line Numbers and Stack Overflow: JTable Line Numbers). However affecting the model may not be the most ideal technique: affecting the model does not provide a general solution that can be reused, and further the model could be required by other features in the application (thus introducing the possible need to reverse the line numbering). Another approach could be to affect the view. This has been done with a JTextArea by Rob Camick, however this is limited to a single component - a JTextArea. Below I pose an alternate method that is independent of the underlying component and relies on the utilization of a row header within a parent JScrollPane.

The JScrollPane allows for both header and row view components - that is components which are displayed above or to the left of the main scrollable Component. This feature can be quite useful when the necessity to draw metrics of the view is required, such as drawing line numbers. To draw the metrics (in this case line numbers), the row header requires access to the metrics of the underlying view, and to do so independent of the type of component it represents I created an interface that defines the line number model:


import java.awt.Rectangle;

/**

 * A generic model interface which defines an underlying component with line numbers.

 * @author Greg Cope

 *

 */

public interface LineNumberModel {



	/**

	 * 

	 * @return

	 */

	public int getNumberLines();

	

	/**

	 * Returns a Rectangle defining the location in the view of the parameter line. Only the y and height fields are required by callers.

	 * @param line

	 * @return A Rectangle defining the view coordinates of the line.

	 */

	public Rectangle getLineRect(int line);



}

The above interface defines two methods:

  1. getNumberLines - this method returns how many total lines the underlying model contains, allowing the caller to iterate over each line.
  2. getLineRect: this method returns a rectangle defining the location in the view the parameter line is located. The caller (below) is only interested in the y and height fields of the Rectangle.

Based upon just these two methods, a caller (below) can draw the line numbers as needed:

import java.awt.Component;

import java.awt.Dimension;

import java.awt.Graphics;

import java.awt.Graphics2D;

import java.awt.Rectangle;

import java.awt.RenderingHints;



import javax.swing.JComponent;

import javax.swing.JScrollPane;

import javax.swing.JViewport;



/**

 * JComponent used to draw line numbers. This JComponent should be added as a row header view in a JScrollPane. Based upon the 

 * LineNumberModel provided, this component will draw the line numbers as needed.

 * @author Greg Cope

 *

 */

public class LineNumberComponent extends JComponent{

	

	static final long serialVersionUID = 432143214L;



	public static final int LEFT_ALIGNMENT = 0;

	public static final int RIGHT_ALIGNMENT = 1;

	public static final int CENTER_ALIGNMENT = 2;

	

	//pixel padding on left and right

	private static final int HORIZONTAL_PADDING = 1;

	//pixel padding on left and right

	private static final int VERTICAL_PADDING = 3;

	

	private int alignment = LEFT_ALIGNMENT;

	

	private LineNumberModel lineNumberModel;

	

	/**

	 * Constructs a new Component with no model

	 */

	public LineNumberComponent(){

		super();

	}

	

	/**

	 * Constructs a new Component based upon the parameter model

	 * @param model

	 */

	public LineNumberComponent(LineNumberModel model){

		this();

		setLineNumberModel(model);

	}

	

	/**

	 * Sets the LineNumberModel

	 * @param model

	 */

	public void setLineNumberModel(LineNumberModel model){

		lineNumberModel = model;

		if ( model != null ){

		    adjustWidth();

		}

		repaint();

	}

	

	/**

	 * Checks and adjusts the width of this component based upon the line numbers

	 */

	public void adjustWidth(){

		int max = lineNumberModel.getNumberLines();

		if ( getGraphics() == null ){

			return;

		}

		int width = getGraphics().getFontMetrics().stringWidth(String.valueOf(max)) + 2 * HORIZONTAL_PADDING;

		JComponent c = (JComponent)getParent();

		if (c == null){//not within a container

			return;

		}

		Dimension dimension = c.getPreferredSize();

		if ( c instanceof JViewport ){//do some climbing up the component tree to get the main JScrollPane view

			JViewport view = (JViewport)c;

			Component parent = view.getParent();

			if ( parent != null && parent instanceof JScrollPane){

				JScrollPane scroller = (JScrollPane)view.getParent();

				dimension = scroller.getViewport().getView().getPreferredSize();

			}			

		}

		if ( width > getPreferredSize().width || width < getPreferredSize().width){

			setPreferredSize(new Dimension(width + 2*HORIZONTAL_PADDING, dimension.height));

			revalidate();

			repaint();

		}

	}

	

	/**

	 * Sets how the numbers will be aligned. 

	 * @param alignment One of RIGHT_ALIGNMENT, LEFT_ALIGNMENT, or CENTER_ALIGNMENT

	 * @throws IllegalArgumentException

	 */

	public void setAlignment(int alignment) throws IllegalArgumentException{

		if ( alignment < 0 || alignment > 2 ){

			throw new IllegalArgumentException("Invalid alignment option");

		}

		this.alignment = alignment;

	}

	

	@Override

	public void paintComponent(Graphics g){

		super.paintComponent(g);

		if ( lineNumberModel == null ){

			return;

		}

		Graphics2D g2d = (Graphics2D)g;

		g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

		g.setColor(getBackground());

		g2d.fillRect(0, 0, getWidth(), getHeight());

		g.setColor(getForeground());

		//iterate over all lines to draw the line numbers.

		for ( int i = 0; i < lineNumberModel.getNumberLines(); i++ ){

			Rectangle rect = lineNumberModel.getLineRect(i);

			String text = String.valueOf(i + 1);

			int yPosition = rect.y + rect.height - VERTICAL_PADDING;

			int xPosition = HORIZONTAL_PADDING;//default to left alignment

			switch (alignment){

			case RIGHT_ALIGNMENT:

				xPosition = getPreferredSize().width - g.getFontMetrics().stringWidth(text) - HORIZONTAL_PADDING;

				break;

			case CENTER_ALIGNMENT:

				xPosition = getPreferredSize().width/2 - g.getFontMetrics().stringWidth(text)/2;

				break;	

			default://left alignment, do nothing

				break;

			}

			g2d.drawString(String.valueOf(i+1), xPosition, yPosition);

		}

		

	}

	

}

The above class utilizes the LineNumberModel interface to draw line numbers as needed. It utilizes the paintComponet method to draw the line numbers based upon the model, and has the flexibility to align the numbers to the left, the right, or in the center. Given it extends JComponent, its font and colors can also be set in the situation where customization is needed.

To see the LineNumberModel and LineNumberComponent in action, below is an SSCCE showing how this framework can be utilized to create line numbers in a JTextArea.


import java.awt.Rectangle;

import javax.swing.JFrame;

import javax.swing.JScrollPane;

import javax.swing.JTextArea;

import javax.swing.SwingUtilities;

import javax.swing.event.DocumentEvent;

import javax.swing.event.DocumentListener;

import javax.swing.text.BadLocationException;

/**

 * Demonstration of the line numbering framework in a JTextArea

 * @author Greg Cope

 *

 */

public class LineNumberTest {

		

	private JTextArea textArea = new JTextArea();

	

	private LineNumberModelImpl lineNumberModel = new LineNumberModelImpl();

	

	private LineNumberComponent lineNumberComponent = new LineNumberComponent(lineNumberModel);

	

	public LineNumberTest(){

		JFrame frame = new JFrame();

		JScrollPane scroller = new JScrollPane(textArea);

		scroller.setRowHeaderView(lineNumberComponent);

		frame.getContentPane().add(scroller);

		lineNumberComponent.setAlignment(LineNumberComponent.CENTER_ALIGNMENT);

		frame.pack();

		frame.setSize(200,200);

		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		textArea.getDocument().addDocumentListener(new DocumentListener(){



			@Override

			public void changedUpdate(DocumentEvent arg0) {

				

				lineNumberComponent.adjustWidth();

			}



			@Override

			public void insertUpdate(DocumentEvent arg0) {

				lineNumberComponent.adjustWidth();

			}



			@Override

			public void removeUpdate(DocumentEvent arg0) {

				lineNumberComponent.adjustWidth();

			}

			

		});

		textArea.setText("This is a demonstration of...\n...line numbering using a JText area within...\n...a JScrollPane");

		frame.setVisible(true);

	}



	

	

	private class LineNumberModelImpl implements LineNumberModel{



		@Override

		public int getNumberLines() {

			return textArea.getLineCount();

		}



		@Override

		public Rectangle getLineRect(int line) {

			try{

				return textArea.modelToView(textArea.getLineStartOffset(line));

			}catch(BadLocationException e){

				e.printStackTrace();

				return new Rectangle();

			}

		}

	}

	

	public static void main(String[] args) throws Exception{

		SwingUtilities.invokeAndWait(new Runnable(){



			@Override

			public void run() {

				new LineNumberTest();

			}

			

		});

	}

}

The above class creates a simple JFrame containing a JTextArea within a JScrollPane - an instance of a LineNumberComponent is used in the row header of the JScrollPane, and a DocumentListener used to update the view of the line numbers based upon changes within the document. The result is a JTextArea with line numbers - a similar example could be made for a JTable, JList, JTree, or any other component provided it is contained with a JScrollPane. The results:

Line Numbering in a JTextArea

Line numbers in a JTextArea

One could extend the framework above to draw different metrics for each line, for instance the number of characters up to that location, a multiple of said line, or a variety of other options that could be necessary.




Comments

  • cpc cpc   -   November, 29, 2013

    It's work for JtextArea but not for JtextPane.

    :(

  • Greg Cope   -   November, 29, 2013

    What doesn't work about it? A JTextPane does not have the utility methods that JTextArea has (getLineCount(), getLineStartOffset()), so the above won't compile with a simple switch to JTextPane. As a result, one must find alternatives for these methods so the LineNumberModel interface can be implemented properly (a JTextPane allows attributes which, if used, can make this non-trivial). That being said, when the LineNumberModel interface is implemented properly the line numbers will show. If not, then post an SSCCE.

  • Humphrey Lopez   -   August, 19, 2015

    Works fine for me. Very nice utillity, and should be part of the swing package. Indeed I only needed it for the JTextArea component. Changed the code a bit to get the Document from the model in the LineNumberComponent.

    Also changed the interface to have an extra method getDocument() that returns the Document from the model.

  • java person that   -   October, 20, 2016

    Took me maybe 20minutes to figure out why your jpanel with number was not working for me. By chance could you move the new textArea, lineNumberModel, lineNumberComponent into the constructor? this would make the copy paste a bit easier.

  • Salinda Dahanayake   -   March, 13, 2017

    DefaultTableModel df = (DefaultTableModel) jTable1.getModel();

    Vector vec = new Vector();

    int ln = df.getRowCount();

    vec.add(ln++);

    * Other Raws

    df.addRow(vec);

Back to Articles


© 2008-2017 Greg Cope