JTable и Serializable или таблицы в Java и танцы с бубном при сохранении объектов в файлы

Created by AOSNEC 2012 (C)

Введение

Так получилось, что как дизайнеру, мне необходим простор для творчества при реализации любых задач в написании программ. Давно я положил глаз на такую платформу как Java, так-как всегда мечтал о кросс платформенном программном обеспечении. И вот недавно, я решил освоить такой прекрасный компонент в Java, как JTable, ну и по той причине, что всегда любил использовать таблицы в своих программах.

В общем, я поставил перед собой не сложную задачу - создать таблицу, которую мог бы сохранять в файл как объект и параллельно отслеживать введенные пользователем данные подсвечивая ошибки и упрощая общение с таблицей моей программы путем подсвечивания наиболее важных элементов таблицы. Так-как я сторонник программирования по принципу пошаговой отладки при написании кода, наличие готовых кусков стабильного кода в сети Интернет, было для меня очень важным... Но... После тщательных поисков, экспериментально было установлено, что, в сети Интернет есть всего пару нормальных источников для получения более или менее хорошего кода.

Нет-нет, я вас не буду отправлять сюда: (Цитирую пользователей javatalks.ru и др. - "перед тем как задать тупой вопрос, посмотрите здесь: www.google.com")

и сюда: (How to Use Tables - tutorial)

или сюда: (Using Swing Components: Examples)

или в стандартный хелп по таблицам.

Хотя, я так говорю не с проста, так-как посмотрев там и поняв, что доки Java немного устарели, как по восприятию, так и по наличию вразумительного рабочего кода примеров (я использовал Java версии 7.01). Но все же, не почитав этих справочных материалов, вы никогда не поймете самой сути, логики и психологии таблиц Java. По этой причине, ниже описанный стабильный пример сериализации таблицы JTable, будет у вас работать так, как вы этого хотите, лишь при условии понимания того, что вы делаете и что хотите получить. Напоследок добавлю - некоторые куски кода были собраны в Интернет на разных ресурсах (за что им всем огромное спасибо), но их всех объединяет одно - я сам их отлаживал и притирал к своей программе, то есть идеально работающего кода я так и не нашел. Хотя... нет. Я построил свой код, только потому, что взял с одного из англоязычных ресурсов рабочий код программы сериализации, который у меня вылетал при попытке изменения данных в таблице (ссылка на ресурс к сожалению потерялась)...

Но "о драконах" кода по порядку.

О как же ты красноречив, дракон великий Error Log

Ну, теперь самое время сказать мне: "Ты че устраиваешь танцы с бубном? Или не читал этого? Гугли получше и не забивай нам всем мозги бирюльками для дизайна... Мы и без него можем обойтись.". И правда, почитав этот перевод книги "Java 2. Том 2. Тонкости программирования. Автора - Кей Хорстман и Гарри Корнелл" (у меня 8-е печатное издание), я обнаружил отличный код:

	
	//запись в файл
	FileOutputStream fos = new FileOutputStream("temp.out");
	ObjectOutputStream oos = new ObjectOutputStream(fos);
	SerialTest st = new SerialTest();
	oos.writeObject(st);
	oos.flush();
	oos.close(); 
	  
	//чтение из файла
	  
	FileInputStream fis = new FileInputStream("temp.out");
	ObjectInputStream oin = new ObjectInputStream(fis);
	TestSerial ts = (TestSerial) oin.readObject();
	System.out.println("version="+ts.version);
                                                

который не при каких обстоятельствах не хотел работать с таблицами, куда бы я не ставил магическую мантру "implements Serializable".

-Да ты не реализуй отдельный класс, а тули сразу в JTable при создании своего TableCellRenderer динамически и в нем делай все, что тебе нужно! - говорили одни...

-Неее... Тебе нужен отдельный класс, ведь Java - это не "хухры-мухры" и модели MVC никто не отменял. - говорили другие.

В общем, все эти споры и предложения реализации классов и методов, которые не работали или работали частично, меня очень расстроили и я даже хотел плюнуть на все и сделать таблицу с дефолтным "серым" графическим интерфейсом... Как вдруг, на одном из зарубежных форумов, обнаружил код, который по уверениям автора работал на все сто процентов и подходит под любые капризы, даже такого дизайнера как я. Внимательно рассмотрев данный код я нашел метод, который создавал шаблон таблицы, если файл отсутствовал на диске и заполнял ним таблицу. После проверки кода (очень близкий к указанному выше), я с удивлением обнаружил, что он работает! "О небо!" - подумал я - "Неужели мои молитвы услышаны и я могу отдать бубен обратно своей малышке дочери?!"... Но стоило мне внести в таблицу данные и нажать кнопку сохранить... как бубен, мне опять стал нужен...

Теперь, это стало делом чести! Я не мог позволить каким-то мантрам, влиять на отсутствие или наличие бубна у моей малышки дочери и на нервные срывы у соседей из-за постоянного звучания бубна... Короче, после короткой битвы с выделением в таблице:

	
	//это работает, но не всегда
	table.clearSelection(); 
                                                

я решил обратиться к шаманам всего мира и к самим создателям... И я пошел на баг-трекер Оракла, где с удивлением заметил, что ошибочка то вот она: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4503845

И длится бой уж много лет, с версии Java "1.4.0-beta2"

Тут я увидел, что особо просветленные шаманы, вроде Kleopatra, там давненько уже отрубают крылья дракона Error Log простым вызовом:

	
	//это я сам вставил, пусть данные сохраняться перед фиксацией таблицы
	//иначе значение сбросится до примитивного "", что приводит к потере данных во всей выделенной строке
	  table.editingStopped(null);
	//а вот, что предложили разработчики Java, по ссылке выше
	/*Цитирую:
	*
	*		The first issue in the description deals with the improper behavior
	*		of losing edits on focus exit the first time but for not subsequent
	*		attempts. The problem was correctly diagnosed by Kleopatra in bug
	*		report 4518907. java.swing.JTable indeed does need to set 'editorRemover'
	*		to null after calls to  
	*		removePropertyChangeListener("focusOwner",  editorRemover);
	*		in both removeNotify() and in removeEditor().
	*
	*/
	//и хоть в английском я не силен, я сделал как они рекомендуют
	table.removeEditor();
	//это можно не писать и без него работает отлично
	table.removeNotify(); 
                                                

Ну вот и всё, дракон уж вроде побежден...

Ан нет, он в классы с TableCellRenderer был перемещен...

Уроки рисования, или "Нет-нет, мне нужно подсветить строку с ошибкой -  красненьким  ..."

После не сложных плясок у нового костра разожженного очередными ошибками визуализации таблицы, я обнаружил, что будет уместным реализовать два класса отрисовки таблицы, один для ячеек и второй для названий (ярлыков) колонок:

	
	//Класс отрисовки таблицы
	class JCTableCellRenderer extends JLabel implements TableCellRenderer, Serializable {...}
	//класс отрисовки ярлыков колонок   
	class JColumnRenderer extends JLabel implements TableCellRenderer, Serializable {...}
                                                

Теперь это все нужно объявить:

	
	//Грузим наш внешний класс рендера таблицы JCTableCellRenderer
	table.setDefaultRenderer(Object.class, new JCTableCellRenderer());  
                                                

В начале метода loadColumn() пишем:

	
	//Используем внешний класс JColumnRenderer для прорисовки TableColumn
	//используется отдельно от JCTableCellRenderer, так-как это ускоряет вывод и существенно упрощает код
	TableCellRenderer renderer = new JColumnRenderer(); 
                                                

Все сразу стало на свои места, все данные отлично читались, записывались и отрисовывались.

Теперь я начал искать в Интернете самое нужное мне, отрисовку ячеек, строк и колонок. Как ни странно, но все ссылки с обещанием именно того, что нужно мне, вели сюда: http://skipy-ru.livejournal.com/1577.html информация ценная, но для меня как для дизайнера, скудная и про TableCellRenderer там говорилось мало (хотя, статья очень достойная внимания). Я начал сам экспериментировать с разными способами окраски таблицы и что я узнал в итоге?

 

	
	//метод внутри класса
	public Component getTableCellRendererComponent(	JTable table, Object value, 
							boolean isSelected, boolean hasFocus, int row, int column) {...}
	  
	//для меня, как дизайнера, означал следующее:
	  
	//@param: table - глобально применяет окрашивание в указанным строкам, ячейкам, столбцам
	//@param: value - применяет окрашивание к указанному значению в ячейке
	//@param: isSelected - это понятно и без объяснений (применяет окрашивание к выделению)
	//@param: hasFocus - применяет окрашивание к выделенной ячейке
	//@param: row - применяет окрашивание в указанной по индексу строке
	//@param: column - применяет окрашивание в указанной по индексу колонке 
                                                

Примеры окрашивания в таблице JTable:


	
	//раз уж мы наследовались от JLabel, то в начале метода ставим, что-то типа
	JLabel c = new JLabel(value.toString());
	//мы будем тыкать лейблы везде, чтобы отобразить все данные таблицы сразу - value.toString()
	
	//если в любой ячейке есть текст "jpg", красим её красивым цветом
	if(value.equals("jpg")){c.setOpaque(true);c.setBackground(new Color(152, 251, 152));}
	
	//первая колонка у нас имеет свой индивидуальный окрас
	if(column==0){c.setOpaque(true);c.setBackground(new Color(255, 248, 220));}
	
	//эта строка у нас будет покрашена в розовый, если в седьмой колонке есть строка со значением "iff"
	if(table.getValueAt(row, 7).equals("iff")){ c.setOpaque(true);
			c.setBackground(new Color(255, 105, 180));
			c.setForeground(Color.white);}
	  
	//проверим два целых числа колонок: четыре и пять, и если условие ложно красим строку красным цветом
	//и... предупреждаем пользователя об ошибке в выбранной колонке нашей таблицы и пишем в модель таблицы
	
	//кстати, не конвертируйте значений так: (String)table.getModel().getValueAt(row, 4)
	//это вызовет ошибку компиляции
	
	int a = Integer.parseInt(""+table.getModel().getValueAt(row, 4));
	int b = Integer.parseInt(""+table.getModel().getValueAt(row, 5));
	if(a > b)
	{
		c.setOpaque(true);c.setBackground(Color.red);c.setForeground(Color.yellow);
		if(column==8)
		{
			c.setText("Error this value: colomn - \"Start\", row - " + (row+1));
			table.getModel().setValueAt(c.getText(), row, column);
		}
	}
	else
	{
		//а это я проверил, вдруг файл на диске не найден, это тоже нужно показать пользователю
		//и восстановить значение в восьмой колонке, если пользователь исправил ошибку
		//хотя, указанным способом, можно просто самим исправить значение на нужное сразу после проверки
		//значений колонок четыре и пять на истинность
		  
		if(column==8)
		{
			java.io.File f = new java.io.File((String) table.getModel().getValueAt(row, 9));
				  
			if(f.exists())
			{
				table.getModel().setValueAt("This file is exist", row, column);
			}
			else
			{
				table.getModel().setValueAt("This file is not exist!!!", row, column);
				c.setOpaque(true);c.setBackground(Color.red);c.setForeground(Color.yellow);
			}
		}
	}
	  
	//этот шикарный код взят с http://skipy-ru.livejournal.com/1577.html
	//его назначение - в зависимости от темы приложения менять стиль таблицы
	  
	// using L&F colors
	if(isSelected)
	{
		c.setOpaque(true);
	
		c.setForeground(isSelected ?
			UIManager.getColor("Table.selectionForeground") :
			UIManager.getColor("Table.foreground"));
		c.setBackground(isSelected ?
			UIManager.getColor("Table.selectionBackground") :
			UIManager.getColor("Table.background"));
		c.setBorder(hasFocus ?
			BorderFactory.createLineBorder(UIManager.getColor("Table.selectionForeground"), 1) :
			BorderFactory.createEmptyBorder(2, 2, 2, 2));
	  
	//а здесь, я проверил на указанные значения ячейки и если они истинны
	//мы изменяем цвет выделенных ячеек на нужный
	  
		if(value.equals("jpg")){c.setOpaque(true);c.setBackground(new Color(0, 128, 128));}
		if(value.equals("png")){c.setOpaque(true);c.setBackground(new Color(250, 250, 210));}
		if(value.equals("iff")){c.setOpaque(true);c.setBackground(new Color(72, 61, 139));
		c.setForeground(Color.white);}
		if(column==0){c.setOpaque(true);c.setBackground(new Color(210, 105, 30));}
	}
                        


Ну вот, вроде бы и все.


Теперь все работает - сохраняется в файл и раскрашивается в нужный цвет.

И это все? А как же быть с ошибками при вводе чисел?!

Да уж... Чуть не забыл о самом главном. При вводе пользователем символов отличных от цифр, программа будет уходить в NumberFormatException. Не порядок - нужно исправить. Но как? И тут я вспомнил один прием, который часто применял во Flash — заглушка. На всякий случай решил погуглить и оказался на очень достойном ресурсе с туторами по Java, в разделе, где предлагалось поставить заглушку по давно используемому мной принципу.

Прийдется переписать упомянутый выше код:

	
	int a = Integer.parseInt(""+table.getModel().getValueAt(row, 4));
	int b = Integer.parseInt(""+table.getModel().getValueAt(row, 5));
	if(a > b)
	{

	//........

	if(value.equals("jpg")){c.setOpaque(true);c.setBackground(new Color(0, 128, 128));}
	if(value.equals("png")){c.setOpaque(true);c.setBackground(new Color(250, 250, 210));}
	if(value.equals("iff")){c.setOpaque(true);c.setBackground(new Color(72, 61, 139));
	c.setForeground(Color.white);}
	if(column==0){c.setOpaque(true);c.setBackground(new Color(210, 105, 30));}
	}
	
                                                

В результате не сложных действий, я решил заглушить ошибку ввода символов не являющихся цифрами, но взамен поставил вложенные try{...}catch{...}, для подмены значений, которые ошибочно ввел пользователь:


	
	try
	{
		//создаем переменные для обработки целых чисел
		int as=0, bs=250, ds=0;

		try
		{
			//проверка на соответствие целому числу
			as = Integer.parseInt(""+table.getModel().getValueAt(row, 4));
			//проверка того, что число является положительным
			if(as < 0){table.getModel().setValueAt(new Integer(0), row, 4);}
		}
		catch (NumberFormatException ex) {
				//если введенные пользователем символы не число заменяем их нулем
				table.getModel().setValueAt(new Integer(0), row, 4);
		}

		try
		{
			//проверка на соответствие целому числу
			bs = Integer.parseInt(""+table.getModel().getValueAt(row, 5));
			//проверка того, что число является положительным
			if(bs < 0){table.getModel().setValueAt(new Integer(100), row, 5);}
		}
		catch (NumberFormatException ex) {
				//если введенные пользователем символы не число заменяем их своим числом
				table.getModel().setValueAt(new Integer(250), row, 5);
		}

		try
		{
			//проверка на соответствие целому числу
			ds = Integer.parseInt(""+table.getModel().getValueAt(row, 6));
			//проверка того, что число является положительным
			if(ds < 0){table.getModel().setValueAt(new Integer(0), row, 6);}
			//проверка того, что число не больше нужного нам значения
			if(ds > bs){table.getModel().setValueAt(new Integer(bs/10), row, 6);}
		}
		catch (NumberFormatException ex) {
				//если введенные пользователем символы не число заменяем их нулем
				table.getModel().setValueAt(new Integer(0), row, 6);
		}

		if(as > bs)
		{
			//если значение ложное красим в красный цвет
			c.setOpaque(true);c.setBackground(Color.red);c.setForeground(Color.yellow);
			if(column==8)
			{
				//здесь мы в восьмой колонке выводим сообщение об ошибке
				c.setText("Error this value: column - \"Start\", row - " + (row+1));
				table.getModel().setValueAt(c.getText(), row, column);
			}
		}
		else
		{
			if(column==8)
			{
				//если все нормально и пользователь исправил ошибку, меняем все назад по дефолту
				java.io.File f = new java.io.File((String) table.getModel().getValueAt(row, 9));

				if(f.exists())
				{
					table.getModel().setValueAt("This file is exist", row, column);
				}
				else
				{
					table.getModel().setValueAt("This file is not exist!!!", row, column);
					c.setOpaque(true);c.setBackground(Color.pink);c.setForeground(Color.red);
				}
			}
		}
	}
	catch (NumberFormatException ex) {

		/**	а это заглушка для не числовых символов введенных пользователем,
		*	пользователи любят озорничать, но мы не дадим им шанса глумиться над нашей программой,
		*	все, что будет вызывать ошибку NumberFormatException, будет глушиться
		*	или перенаправляться во внутренние try{...}catch{...} на дополнительную обработку
		*	не рекомендуется злоупотреблять таким способом подавления ошибок, так-как к этому можно привыкнуть
		*	и это может привести к серйозным сбоям в вашей программе
		**/

		;
	}
	
                        

Такая реализация проверки введенных данных и подмены значений не являющихся целыми числами или числами которые не отвечают некоторым требованиям - работает стабильно.

С чем вас всех и поздравляю.


Для реализации своих идей я использовал среду разработки Eclipse v. 3.7


Ну и... Если вы дочитали до этого места, то спасибо вам за потраченное время.

Если вы хотите задать вопрос типа: "А как мне это сделать с базой данных?" или "Почему, реализуя циклы типа for при проверке значений в классе JCTableCellRenderer у меня ужасно тормозит таблица/система", то пожалуй добавлю, что на базах данных выше указанного кода я не применял (но почему-то уверен, что он работает стабильно), и еще - циклы реализованные в классах рисования, это такое же извращение как и таймеры на таймлайне клипов во Flash.
Рекомендую всегда оптимизировать свой код по принципу - "чем проще, тем быстрее".

Нужные внешние классы и код самой программы прилагаются (открывать в Eclipse) .


Удачи всем.

 

(Оригинальная статься (C) - Автор, Петр Бган, 2012 год)