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

Введение


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

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

Нет-нет, я вас не буду отправлять сюда (Популярная цитата пользователей javatalks.ru и др. — «перед тем как задать тупой вопрос, посмотрите здесь...»).

и сюда. (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();


я решил обратиться к шаманам всего мира и к самим создателям… И я пошел на баг-трекер Оракла, где с удивлением заметил, что ошибочка то вот она.

И длится бой уж много лет, с версии 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();


Все сразу стало на свои места, все данные отлично читались и отрисовывались.
Теперь я начал искать в Интернете самое нужное мне, отрисовку ячеек, строк и колонок.
Как ни странно, но все ссылки с обещанием именно того, что нужно мне, вели сюда.

информация очень ценная, но по дизайну скудная и про 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));}
}


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

Теперь все работает — сохраняется в файл и раскрашивается в нужный цвет.
Для реализации своего замысла я использовал среду разработки Eclipse v. 3.7

image

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

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

Удачи всем.

Похожие публикации

Средняя зарплата в IT

113 000 ₽/мес.
Средняя зарплата по всем IT-специализациям на основании 10 037 анкет, за 2-ое пол. 2020 года Узнать свою зарплату
Реклама
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее

Комментарии 21

    +2
    Эта чуть более развернутая и удобочитаемая статья с подсветкой синтаксиса и подробным разбором перехвата NumberFormatException при перерисовке таблицы здесь. По мере необходимости статья будет дополняться и улучшаться.
      +3
      WTF did I just read?!
      в тегах есть упоминание mvc… так чего же вы объединяете все в одно?! сделали бы отдельный класс для хранения данных, не было бы никаких проблем с сериализацией… зачем сохранять полностью объект таблицы?!
        +1
        … зачем сохранять полностью объект таблицы?!

        Например, при перемещении/масштабировании пользователем колонок, сохранится декорирование.
        На выходе получается бинарный файл.
        Нет необходимости использовать базу данных и её драйвера.
        Сохраняются все введенные данные, а значит при запуске программы не нужно в циклах заполнять и декорировать таблицу, так-как это происходит автоматически.
          +3
          Только есть одна проблема при сериализации Swing-компонентов, описанная прямо-таки в исходнике каждого J-компонента:
          * Warning:
          * Serialized objects of this class will not be compatible with
          * future Swing releases. The current serialization support is
          * appropriate for short term storage or RMI between applications running
          * the same version of Swing. As of 1.4, support for long term storage
          * of all JavaBeansTM
          * has been added to the java.beans package.
          * Please see {@link java.beans.XMLEncoder}.


          В общем, кратко говоря, ваши данные в таком виде могут неожиданно стать нечитаемыми при переходе на более новые версии Java. А возможность сериализации подобных компонентов добавлена лишь для кратковременного хранения/передачи компонентов между Java приложениями.
            +1
            P.S. Цитату зял прямо из исходника javax.swing.JTable
            0
            Тоже не понимаю зачем сериализовывать объект JTable.
            Можно например сериализовать модели: TableModel и TableColumnModel
          +5
          По принципу китайских пионеров, сами придумали себе проблему, сами ее и решили :)

          За страдания — плюс, но вообще так никогда не надо делать. Визуальное представление не должно быть перемешано с логикой хранения и загрузки данных, это совершенно излишняя связность. Неудивительно, что вы встретили столько проблем при реализации.
            +1
            Большое спасибо за плюс и отзывы.
            Хочу заметить, что в архиве, в папке «testTable\aosnec» было три файла:

            testTable.java — здесь класс визуального представления,
            JCTableCellRenderer.java — класс реализации прорисовки таблицы,
            JColumnRenderer.java — класс прорисовки ярлыков колонок.

            Да, в testTable реализованы методы создания или загрузки колонок и таблицы: loadColumn() и loadModel(). Их и вправду лучше реализовать в отдельном сериализованном классе. Честно говоря, изначально я так и делал, но код все равно не хотел работать из-за попыток сериализации выделения в таблице:

            java.io.NotSerializableException: javax.swing.JTable$CellEditorRemover

            По этому для простоты перемещения по коду я реализовал все вместе и создал маркеры TODO, чтобы проще было искать основные методы. Критику учел, поместил все в отдельный класс TableAndColumnCreater и переложил в архив.

            Теперь в архиве, в папке «testTable\aosnec» четыре файла:

            testTable.java — здесь класс визуального представления,
            JCTableCellRenderer.java — класс реализации прорисовки таблицы,
            JColumnRenderer.java — класс прорисовки ярлыков колонок,
            TableAndColumnCreater.java — класс создания таблицы и колонок.

            Все это можно скачать по вышеуказанной ссылке в статье.
            +2
            Я все понимаю, но сериализовать следовало данные, а не визуальный компонент-таблицу. А таблицу можно было просто научить корректно отрисовывать полученные данные (с позиционированием колонок, цветами и прочим).

            При описанном вами подходе вы не сможете быстро и дешево изменить свое приложения, если требования к UI изменятся (скажем, если потребуется что-то, что таблицей не является, или будет целый набор связанных контролов, например добавится еще иерархическая структура, просмотр «мастер-детали» и т.п.).
              +2
              Извините, промахнулся веткой. Отвечал на этот комментарий.
                +1
                На счет необходимости сериализации данных можно долго спорить (по крайней мере баз данных или таблиц которые с ними работают). И такое я вряд ли буду сам практиковать, так-как это не только чревато порчей объектов в базах данных или самих таблиц и базы данных при транзакциях, но и грозит серьезными утечками памяти при количестве записей в базе больше сотни или двух. По этому сериализация хороша именно для маленьких приложений с табличным отображением данных, удобным для восприятия подавляющего большинства пользователей.
                Но не могу не согласиться, что такой подход именно для таблиц может быть и впрямь излишен, как заметил выше knekrasov. Но ведь так хочется сериализовать сложные объектные структуры, содержащие визуальные элементы, и необходимые им уникальные данные. Особенно, такой подход ИМХО будет хорош для создания нодовых графических узлов (не содержащих в себе медиа контент). Извините, я живу, работаю и мыслю как дизайнер.
                  +1
                  Видимо, вы действительно мыслите как дизайнер.

                  Все, что я пытаюсь донести — не перемешивайте несвязанные между собой задачи в одной сущности. Данные — отдельно, бизнес-логика — отдельно, представление — отдельно. Вы же зачем-то упомянули MVC в тегах?

                  Ваши доводы про «порчу таблиц при транзакциях» и про утечки памяти мягко говоря не обоснованы.
                • НЛО прилетело и опубликовало эту надпись здесь
                  +1
                  Вообще в своё время я подумывал о таком хранении данных из приложения (просто сливать сериализовать все нужные компоненты в файлы и потом считывать), но по весьма весомым причинам, приведённым выше, я ушёл от данного «брутального» способа хранения.

                  Вы правы, что помимо данных таблицы при такой сериализации сливаются ещё и:
                  — выделение в таблице
                  — все изменения размеров колонок
                  — сортировка колонок
                  и пр. мелочи.

                  Ну, что тут можно сказать, никто не мешает отдельно или даже вместе с данными хранить (и обновлять при изменении) информацию о выделении/колонках/сортировке. Мы именно так и поступили.

                  И ещё одна небольшая мелочь — мы решили уйти от стандартной сериализации объектов и стали широко использовать библиотеку XStream, которая умеет легко и быстро (а также с тонной опций) переводить сериализуемые объекты в читаемые XML файлы. Советую попробовать — останетесь довольны результатом.

                  P.S. Если интересно — могу более подробно описать XStream/наши варианты сохранения таблиц и пр. информации.
                    0
                    Спасибо большое, обязательно буду использовать.
                    +1
                    Слушайте, бросайте программирование, из этого все равно ничего путного не выйдет. Рисуйте лучше макеты, если вы дизайнер.
                      0
                      Ага. В 2001 году я уже послушал один раз совета не применять в интерфейсах программ разные цвета и градиентные переходы и (по тому же совету) пошел работать в дизайн наружной рекламы. Теперь жалею. Недавно заезжал на старую работу (ту, где создал первую программу с цветным графическим интерфейсом — «Калькулятор прихода/расхода») увидел её и почувствовал себя обманутым школьником… Те глянцевые, контрастные кнопки, меню и списки, которые сейчас применяются (в подавляющем большинстве) в дизайне интерфейсов программ меркнут перед этим, действительно рожденным в муках дизайном интерфейса. Хотя, на вкус и цвет…

                      Но я не живу прошлым.

                      Несколько лет назад, у меня появилась идея — объединить то, что сейчас все активно разделяют (код и графическое представление). Не в смысле вернуться к старому типу программирования, а создать визуальную архитектуру взаимодействия объектов (и графики, и логики, и данных). В общем, я пока еще экспериментирую «в песочнице» Java. Но скоро, чего нибудь придумаю. Конечно есть нодовая система представления и UML, но тут камнем преткновения является — удобочитаемость графического представления. Велосипед с формами давно устарел, как и общепринятые элементы графического интерфейса…

                      Уверен — в написании кода, именно за графикой будущее, так-как это убивает сразу всех зайцев: дешевле, красивее, понятнее. И то, над чем мы все сейчас трудимся — создание идеальных алгоритмов, вскоре достигнет уровня, когда таки и простая домохозяйка сможет набросать в три клика программку многоуровневого доступа к реляционным базам данных любимого магазинчика, с дисплея холодильника. И если вы её (домохозяйку) спросите о типе шаблона который она применяла при проектировании своей программы, то она вам не только скажет MVC, MVVM и/или др. типа «Фабрика», «Одиночка», «Наблюдатель» и т.д., но и картинками на дисплее того же холодильника покажет, как она это делает и какой эффект от этого получится при покупке товаров (или в каком магазине, какой шаблон используют). Хотя имена шаблонов к тому времени, наверное, тоже изменятся. Вот к этому мы все и стремимся. На том и будем стоять, и я, и создатели Java.
                        0
                        Я же не писал, что плохо использовать разные цвета в интерфейсе. Я вам написал этот комментарий, потому, что вместо того, чтобы в чем-то разобраться, вы тупо копируете куски кода из интернет и ищете ответы на форумах (а надо — в мануалах и исходных кодах). Возможно, какую-то простую программку таким методом и можно написать, но если вы захотите сделать более серьезный проект, или работать в команде, этот подход в итоге провалится. Более того, такой подход, когда человек досконально не разбирается в используемых инструментах, чреват багами.

                        Но потом я еще подумал, мне кажется, что и с проектированием интерфейсов у вас не очень. Возьмем, например, скриншот таблицы, приведенный в конце статьи. Что мы видим, глядя на него? Бессмысленные иконки в заголовке каждой колонки. Часть иконок не узнаваема, часть непонятно что значит, зачем они поставлены тут, непонятно, видимо по принципу «чтобы были». Стоит ли говорить, что в данном случае вместо пользы (быстрое нахождение нужного элемента) иконки представляют собой визуальный мусор, отвлекающий и замедляющий анализ представленной информации? Возможно, автор не знает, зачем используются иконки и значки в интерфейсах и бездумно повторяет то, что увидел где-то в другом месте?

                        Кнопки сгруппированы внизу без всякого смысла и логики. Надписи в ячейках прилипают к разделительным линиям без всякого отступа. Сочетания красного и желтого цветов плохо читаемы.

                        А ведь все эти вещи уже давно изучены и описаны, мне кажется, вам стоило бы почитать не только книжки по Java, но и книжки на тему usability/ux design.

                        Что касается идеи «визуальной архитектуры» — а давайте, нарисуйте пару концептов, напишите статейку, хабрапользователи, уверен, с удовольствием ее обсудят и укажут вам на все недостатки. Хотя я конечно, сильно сомневаюсь, что описанное вами вообще возможно.

                        > создание идеальных алгоритмов, вскоре достигнет уровня, когда таки и простая домохозяйка сможет набросать в три клика программку многоуровневого доступа к реляционным базам данных любимого магазинчика, с дисплея холодильника

                        Не достигнет. Она бросит затею на попытке понять, что такое вообще реляционная база данных (если они все еще будут существовать).

                          0
                          Я сдаюсь. Но… таблица выше, это всего лишь «пример» и дизайна в ней нет вообще (особенно моего). Это такая «глиняная» заготовка из которой можно слепить то, что нужно… Правда если руки растут из того места. То, что получится из этой «глины» вы скоро увидите, когда я закончу свою программу. Признаюсь по секрету, табличная форма представления (для меня) очень громоздка сама по себе. В общем, я над этим работаю. Но за критику и очень хорошие советы — большое спасибо.
                      0
                      Мне кажется компонент JTable можно использовать только для каких нибудь очень простых таблиц, потому что не очень он удобный.

                      Для более сложных таблиц можно использовать такой компонент как JxCell это таблица аля Excel.
                      Правда она платная.
                      Но зато в ней есть много фич как в Excel например формулы в ячейках и другое.
                        0
                        Спасибо за ссылку, но я сторонник ОЧЕНЬ чистого API.

                        Такое легко получить в JTable, примеров много в сети Интернет, например вот или здесь.
                        Всего лишь, пишем пару малюсеньких классов дополнительных и все. А по формулах отдельный разговор, как я писал в своей статье здесь, это легко проверяется при перерисовке таблицы. Создаем свои «макро-функции» и все. Главное не использовать циклов, это часто вешает программу.

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое