Пример GUI клиента к 1С на Java.

Постановка задачи

Для демонстрации возможности построения "толстого" клиента с помощью J1C демонстрируется GUI клиент на Java.

Реализация задачи будет показана на примере просмотра справочников.

Клиент делаем на основе Swing пакета. Клиент может подключаться к любой 1C базе (SQL или DBF) и дает возможность просматривать любой справочник.

Если справочник иерархический, то должно быть показано дерево.

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

Сначала покажем готовый результат. Затем приведем часть кода, на который следует обратить внимание с т.з. использования J1C(Исходный код полного приложения можно загрузить, используя ссылки, приведенные в разделе "Загрузка" данного руководства.)

Так выглядит главное окно и с настройками подключения (можно подключаться к SQL учетной записью той же, что и сама 1С или иной записью).

Настройки подключения

Выбираем нужный справочник

Выбор справочника
А так выглядит иерархический справочник после подключения и его выбора.
Просмотр

Исходный код

Приводить весь исходный код мы не будем. Кому интересно, может его скачать полностью.

Приведем только фрагменты, на которые стоит обратить внимание.

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

Для показа данных мы используем JTable. Кроме того, так же, как и сама 1С будем использовать TYPE_SCROLL_SENSITIVE CONCUR_READ_ONLY курсор. Т.е. мы можем бегать курсором по данным взад и вперед и при этом считываем измененные данные.

Кроме того, нужно так же оптимизировать вывод полей - так как реквизитов в справочнике может быть тоже много, которые выведены на просмотр, то нужно сделать, что чтение данных было один раз на одно обновления буфера.

Основное действие разворачивается в модели данных таблицы. Приведем его полностью.

	class CatalogTableModel extends AbstractTableModel{
		private static final int SIZEBUF = 1;//коэф. неотображаемых строк буфера от кол-ва видимых строк
		private ArrayList<Object[]> buf = new ArrayList<Object[]>();
		private RecordSet rs;
		private ResultSet cursor;
		private int count;
		private boolean lastMoveForward = false; //true - forward, false - backward
		
		private final int columnCount;
		private final String[] columnName;
		
		private String query;
		
		@Override
		protected void finalize() throws Throwable {
			cursor.close();
		}
		
		public CatalogTableModel() {
			
			
			int result = 1;
			if(meta.getLengthCode()>0) result++;
			if(meta.getLengthName()>0) result++;
			columnCount = result+=meta.getPropertys().size();

			columnName = new String[columnCount];
			
			int ind =0;
			columnName[ind]="";
			ind++;
			if(meta.getLengthCode()>0){
				columnName[ind]="Код";
				ind++;
			}
			if(meta.getLengthName()>0){
				columnName[ind]="Наименование";
				ind++;
			}
			for(Iterator<Metadatas.Property>i=meta.getPropertys().iterator();i.hasNext();){
				columnName[ind]=i.next().getName();
				ind++;
			}
			
			rs = v7.new RecordSet(V7.RecordSet.TYPE_SCROLL_SENSITIVE, V7.RecordSet.CONCUR_READ_ONLY);
			query = "select * from $Справочник."+meta.getName();
			if(grouping&&meta.getCountLevel()>1) {
				query+=" where parentid=:parent  order by isFolder"+(sortField!=null?","+sortField:"")+",id,row_id";
			}
			else{
				query+=" order by "+(sortField!=null?sortField+",":"")+"id,row_id";
			}

			
			init();
		}
		
		@Override
		public String getColumnName(int column) {
			return columnName[column];
		}
		

		@Override
		public Class<?> getColumnClass(int columnIndex) {
			return getValueAt(0, columnIndex).getClass();
		}
		
		void fillBufUp(){
			if(lastMoveForward){
				int ind=0;
				while(cursor.previous()&&ind<buf.size()){
					ind++;
				}
			}
			//должны стоять на строке соответствующей первой строке буфера
			cursor.next();
			count=0;
			while(cursor.previous()&&count<SIZEBUF*rowScreen){
				buf.add(0, getRowBuf());
				count++;
			}
			if(count>0){
				fireTableRowsInserted(0, count-1);
				initColumnSizes();
			}
			
			lastMoveForward=false;
			truncateDown();
		}
		
		void fillBufDown(){
			
			if(!lastMoveForward){
				int ind=0;
				while(cursor.next()&&ind<buf.size()){
					ind++;
				}
			}
			//должны стоять на строке соответствующей последней строке буфера
			cursor.previous();
			count=0;
			while(cursor.next()&&count<SIZEBUF*rowScreen){
				buf.add(getRowBuf());
				count++;
			}
			if(count>0) {
				fireTableRowsInserted(buf.size()-1-count, buf.size()-1);
				initColumnSizes();
			}
			
			lastMoveForward=true;
			truncateUp();
		}
		
	    /**Процедура нужна для того, что бы переустанавливать размеры колонок после обновления
	     * буфера
	     * 
	     */
	    private void initColumnSizes() {
	        TableColumn column = null;
	        Component comp = null;
	        int headerWidth = 0;
	        int columnWidth = 0;
	        int midleSize = 4;
	        TableCellRenderer headerRenderer = table.getTableHeader().getDefaultRenderer();

	        for (int i = 0; i < columnCount; i++) {
	            column = table.getColumnModel().getColumn(i);
	            
	            
	            comp = headerRenderer.getTableCellRendererComponent(
	                                 null, column.getHeaderValue(),
	                                 false, false, 0, 0);
	            headerWidth = comp.getPreferredSize().width;
	            
	            if(i==0) {
	            	column.setResizable(false);
	            	columnWidth = 0;
	            }
	            else if(columnName[i].equals("Код")){
	            	columnWidth = midleSize*meta.getLengthCode();
	            }
	            else if(columnName[i].equals("Наименование")){
	            	columnWidth = midleSize*meta.getLengthName();
	            }
	            else{
	            	Property property =meta.getProperty(columnName[i]); 
	            	if(property.getType() instanceof Row){
	            		columnWidth = midleSize*property.getLength();
	            	}
	            	else if(property.getType() instanceof Date){
	            		columnWidth = midleSize*20;
	            	}
	            	else if(property.getType() instanceof Numeric){
	            		columnWidth = midleSize*property.getLength();
	            	}
	            	else{//далее здесь нужно прописать аккуратно все типы
	            		//и "подбирания" оптимальной" длины каждого
	            		//но мы сделаем проще (ведь пример все-таки)
	            		columnWidth = midleSize*30;
	            	}
	            }
	            
	            column.setPreferredWidth(Math.max(columnWidth, headerWidth));
	        }
	    }
		
		void init(){
			
			buf.clear();
			rs.setParameter("parent", parent);
			cursor = rs.executeQuery(query);
			fillBufDown();
		}
		
		private void truncateUp(){
			int possible = Math.min(count, buf.size()-count-rowScreen-rowScreen*SIZEBUF);
			int ind = possible;
			while(ind>0){
				buf.remove(0);
				ind--;
			}
			if(possible>0){
				fireTableRowsDeleted(0, possible-1);
			}
		}

		private void truncateDown(){
			int possible = Math.min(count, buf.size()-count-rowScreen-rowScreen*SIZEBUF);
			int ind = possible;
			while(ind>0){
				buf.remove(buf.size()-1);
				ind--;
			}
			if(possible>0){
				fireTableRowsDeleted(buf.size(), buf.size()+possible-1);
			}
		}

		private Object[] getRowBuf() {
			Object[] temp = new Object[columnCount];
			
			CatalogReference ref = (CatalogReference)cursor.getRef("id", meta);
			
			int ind = 0;

			if(ref.isFolder()) temp[0]=folderIcon;
			else temp[0]=leafIcon;
			ind++;
			
			if(meta.getLengthCode()>0) {
				temp[ind]=ref.getCode();
				ind++;
			}
			if(meta.getLengthName()>0){
				temp[ind]=ref.getName();
				ind++;
			}
			
			for(int i=ind;i<columnName.length;i++){
				temp[i]=ref.getAttribute(columnName[i]);
			}
			
			return temp;
		}

		@Override
		public int getColumnCount() {
			return columnCount;
		}

		@Override
		public int getRowCount() {
			return buf.size();
		}

		@Override
		public Object getValueAt(int rowIndex, int columnIndex) {
			return buf.get(rowIndex)[columnIndex];
		}
	}

Получение данных из курсора происходит в методе getRowBuf(). Обратите внимание на место, где происходит обращение к ссылке объекта 1С:

...
	temp[i]=ref.getAttribute(columnName[i]);
...
...

Метод getAttribute() ссылочного объекта справочника получает атрибут справочника и возвращает ссылочный объект.

Момент, на который хотим обратить внимание, это мест в конструкторе, где мы строим строку запроса к базе:

...
query = "select * from $Справочник."+meta.getName();
...

Метаподстановки в J1C поддерживают нотацию 1CPP. Здесь конечно приведен простой пример, но возможно и так:

...
query = "select * from $Справочник.Сотрудники.Оклад(:id, :data)";
...

или так:

...
query = "select id as [id $Справочник.Сотрудники] from $Справочник.Сотрудники";
...

с последующим использованием автотипизации через метод rs.getObject()

Заключение

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

Так же нет никаких ограничений работы с другими объектами, помимо справочников. J1C предоставляет доступ ко всем объектам 1С.

Ресурсы

Инструкция: кликните по имени файла для начала скачивания

Описание и имя файла Размер
gui-0.0.2.jar
gui-0.0.2.jar
3 641 K