Курсовая работа студента 342 группы Матвеева Н. Н. «Реализация отмены/повтора некоторых пользовательских действий в редакторе qreal»




Скачать 141.24 Kb.
Название Курсовая работа студента 342 группы Матвеева Н. Н. «Реализация отмены/повтора некоторых пользовательских действий в редакторе qreal»
Дата публикации 27.05.2014
Размер 141.24 Kb.
Тип Курсовая
literature-edu.ru > Курсовая работа > Курсовая
Курсовая работа студента 342 группы Матвеева Н.Н.

«Реализация отмены/повтора некоторых пользовательских действий в редакторе QReal»
Научный руководитель: Шубочкина Т.А.

1. Введение.
Среди многочисленных видов инструментальных средств разработчиков и проектировщиков ПО можно отдельно выделить так называемые CASE-средства (CASE - Computer-Aided Software Engineering). Этот класс инструментов позволяет существенно облегчить и автоматизировать разработку ПО и избавить программистов от рутинной (а порой и невыполнимой) работы.
QReal – CASE-средство, основанное на идеях REAL, позволяющее создавать свои собственные классы диаграмм, редактировать их и генерировать по ним некоторый код. Разработка ведется уже два года (с середины 2007 года) и на данный момент созданы все основные компоненты системы.

2. Архитектура QReal.
QReal можно разбить на три основные компоненты:


  1. Репозиторий

  2. Редактор диаграмм

  3. Кодогенераторы



1) В репозитории хранится редактируемая модель. К нему могут подключаться несколько пользователей и вести совместную работу над моделью. Имеет собственное представление данных и интерфейс для доступа клиентских редакторов диаграмм.
2) Редактор диаграмм является пользовательским интерфейсом для доступа к репозиторию. При подключении к репозиторию, редактор синхронизируется с ним и строит собственное графическое представление модели, хранимой в репозитории.
3) Пользователь может создавать собственные диаграммы для моделирования своих задач. Помимо визуального представления модели, пользователь может реализовать соответствующий кодогенератор и по построенной модели получить необходимый ему код.
Редактор диаграмм QReal состоит из двух компонент:


  1. RepoClient

  2. GUI


1) RepoClient – компонента, обеспечивающая доступ к репозиторию через его интерфейс. Обладает собственным интерфейсом и умеет обрабатывать ответы сервера.

2) GUI – (сокращение от “Graphical User Interface”) по сути, и является тем, что называется редактором. Эта компонента предоставляет пользователю графический интерфейс и обладает собственной (более близкой к графической, чем в репозитории) моделью данных хранимых в репозитории.

Исследования в данной курсовой работе связаны только с редактором диаграмм, а точнее с GUI. Поэтому далее опишем подробнее эту компоненту.

GUI, как и весь QReal, реализован на С++ с использованием кроссплатформенной библиотеки Trolltech Qt4 (что обеспечивает QReal кроссплатформенность на уровне перекомпиляции исходных кодов).

Архитектура GUI соответствует парадигме Model/View (хотя на самом деле, модель(Model) в GUI сама является представлением(View) другой модели, хранимой в репозитории).

Для хранения модели используется класс RealRepoModel – реализация стандартного абстрактного класса QAbstractItemModel.

GUI содержит в себе несколько представлений (View) своей модели: сцена (Scene), обозреватель диаграмм (Diagram Explorer), обозреватель объектов (Object Explorer), миникарта (MiniMap).


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


Концепция отделения модели от представления согласно парадигме Model/View/Controller(MVC)([1]), предполагает, и это видно из самого названия, помимо модели и представления, еще и контроллер(Controller). Пользователь как-то меняет модель через графический интерфейс, предоставляемый ему представлением. Представление, согласно MVC не может менять модель данных, поэтому оно оповещает о действиях пользователя контроллер, который в свою очередь интерпретирует пользовательские действия и меняет модель данных.
Разработчики QReal по причине громоздкости архитектуры MVC([2]) строили редактор диаграмм по несколько более упрощенной парадигме – Model/View(MV).
При таком подходе контроллер необходимо интегрировать либо в представление, либо в модель.
То есть либо представление должно знать, как интерпретировать действия пользователя, либо модель.
Редактору QReal, как и любому редактору с интерактивным пользовательским интерфейсом необходима возможность отменять (и соответственно повторять в случае отмены) пользовательские действия. Пусть пользователь совершает некоторое действие и меняет конфигурацию модели - отмена пользовательского действия заключается в возвращении модели в ту же конфигурацию, в которой она находилась до действия пользователя. Также нужна возможность повторять пользовательские действия, то есть восстанавливать конфигурацию модели после отмены.
Чтобы не хранить всю модель до и после совершенного действия целиком, можно обращать действия пользователя, т.е. при отмене, изменять модель так чтобы она перешла в прежнюю конфигурацию. В случае, например, графических растровых редакторов (Photoshop, GIMP), не всегда можно изменить модель так, чтобы она перешла в прежнюю конфигурацию, например при наложении фильтров и сжатии изображения. Поэтому приходится хранить модель (или, по крайней мере, ее часть) целиком для каждого такого, необратимого действия.

3. Постановка задачи.
Реализовать возможность отмены/повтора пользовательских действий в редакторе QReal.

4. Undo Framework.
Реализация отмены/повтора:
Выше упоминалось, что каждое действие пользователя переводит модель в новую конфигурацию и про то, что отмена/повтор действий — это возможность переходить из текущей конфигурации в любую конфигурацию в прошлом и возвращаться обратно. Чтобы не хранить конфигурации целиком, можно хранить какие-то небольшие части данных, используя которые, можно вернуться к предыдущей конфигурации. Исходя из этого, можно вместо старой конфигурации хранить лишь то, что изменилось, и способ, как используя эти данные вернуть конфигурацию к прежнему состоянию. А также способ, как перевести конфигурацию из старого состояния в новое. Соответственно, можно выделить класс command, который будет этим заниматься. Он будет обладать методом redo(), undo() и инкапсулировать данные необходимые для отката/повтора конфигурации.

Данные будут инициализироваться частично при первом вызове redo() и частично в конструкторе.
То есть действия пользователя по изменению модели сопровождаются добавлением таких команд в определенный стек (undo stack). При добавлении новой команды, у нее вызывается redo() и затем она кладется в undo stack. При отмене снимается верхний элемент (команда) и вызывается у него undo(), а сам элемент кладется в redo stack( в котором вызывается redo() у команды, в случае повтора действий). При добавлении новой команды (т.е. изменении модели), redo stack опустошается.

Подробнее об этом можно прочитать в [3].

Возможности QUndoStack.
В библиотеке Qt4 уже есть реализация достаточно универсального Undo stack – QundoStack([4]). Команды, с которыми он работает, должны наследоваться от класса QUndoCommand. Среди всех его возможностей, нас особенно интересует возможность создания сложных команд. Расскажу об этом подробнее.
Класс QUndoStack обладает двумя методами: beginMacro() и endMacro(), и все команды, которые были положены в стек после вызова beginMacro(), и перед endMacro()(заметим, что расположение в стеке гарантирует их упорядоченность), будут мыслиться с точки зрения стека как одна команда. То есть, если вызвать затем у QUndoStack метод undo(), то QUndoStack по порядку снимет команды (и вызовет для каждой undo()) с вершины стека до, условно, метки, когда был вызван beginMacro(). И наоборот, метод redo() в случае макроса, выполнит все команды до вызова endMacro(). Также следует добавить, что такие макросы реентерабельны (то есть допускаются вложенные макросы).
Команды, укладываемые в стек, между beginMacro() и endMacro(), мыслятся как единая команда:



5. Редактор QReal.
Модель:
Рассмотрим архитектуру модели в QReal. Как упоминалось ранее, доступ к модели RealRepoModel может быть осуществлен через интерфейс QAbstractItemModel([5]) Модель представляет собой самое обычное дерево, которое реализует структура RepoTreeItem. Каждый объект типа RepoTreeItem содержит указатель на своего родителя, список указателей на детей, а также несколько специфических полей (включаю идентификатор этого объекта в репозитории). Любая сущность визуальной модели в редакторе QReal (будь то вершина или ребро) физически хранится в этом дереве и логически отличается от других таких же элементов своим положением в дереве и значением ролей. Роль - стандартное понятие библиотеки Qt и интерфейса QAbstractItemModel в частности, это дескриптор некоторого атрибута у объекта, который хранится в модели. Можно использовать стандартные роли (например Qt::DisplayRole, Qt::EditRole) и можно определять свои(Qt::UserRole).

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


  • Изменение свойств объекта (позиция на экране, размер на экране, атрибуты)

  • Добавление/Удаление объектов

  • Вырезать-Вставить/Копировать-Вставить для объектов



Проблемы:


  1. Undo stack частично реализован, для него определенны только команды отмены для изменения ролей объектов модели. Но реализация команд ненадежна. Необходимо доделать остальные команды и переписать существующие.




  1. Существующие сейчас методы для добавления(addElementToModel) и перемещения(changeParent) громоздки и трудночитаемы. Не представляется возможным интегрировать их в undo stack. Необходимо их перепроектировать и упростить.




  1. Часть логики графического интерфейса переложено на методы модели. Например, в методе addElementToModel, который расположен в модели, создается выпадающее меню для представления. Необходимо отделить логику графического интерфейса от модели.




  1. У некоторых объектов (отношения, например) есть атрибуты, значениями которых могут быть идентификаторы объектов. Нужно при удалении обрабатывать это (в соответствии с соглашением по этому поводу среди разработчиков) и соответственно, возвращать все на место в случае повтора.

6. Решение поставленной задачи. Архитектура команд.
Существует два способа реализовать команды: можно дать командам возможность изменять модель напрямую, либо обеспечить для этого интерфейс. Второй способ предпочтительней, так как тогда можно избежать дублирования и обеспечить простоту модификации кода. Так как команд в QReal предполагается не так много, то можно не заниматься выделением специального интерфейса. Однако некоторые методы все-таки удобнее будет выделить.
Заметим, что класть команды в стек напрямую из графического интерфейса не совсем правильно, потому что, во-первых, это усложнит рефакторинг кода (например, если изменится имя какой-то команды, то придется менять его во всех местах, где она кладется в стек), а во-вторых, добавит ненужные трудности при реализации составных команд (то есть в тех случаях, когда одно пользовательское действие разбивается на несколько команд).
1) Изменение роли.
Начнем с самой простого действия — изменить роль у данного объекта.
Для этих целей в интерфейсе QAbstractItemModel есть метод setData():

bool setData(const QModelIndex & index, const QVariant & value, int role = Qt::EditRole);

Так как графический интерфейс позволяет пользователю изменить конкретную роль у конкретного объекта, то API модели должно содержать метод с такой же сигнатурой. Выделим для этого специальный метод changeRole():

bool changeRole(const QModelIndex & index, const QVariant & value, int role = Qt::EditRole);
Метод changeRole() получает данные, проводит специфические проверки(например смотрит, изменилась ли на самом деле позиция объекта или нет) и добавляет, в зависимости от полученной роли, команды. Потребовалось реализовать ChangePositionRoleCommand, ChangeConfigurationRoleCommand, ChangeEditRoleCommand, ChangeUserRoleCommand которые наследуются от команды ChangeRoleCommand, так как в принципе имеют схожее поведение. В самих командах хранится старое значение, новое и идентификатор объекта(его id в репозитории). Метод undo() такой команды вызывает метод setData(), в котором подставляется старое значение роли. Соответственно redo() вызывает setData() с новым значением. Пример реализации команды ChangeUserRole в Приложении 1.
2) Добавление нового элемента.
Далее рассмотрим действие — добавить новый элемент.
Действие заключается в том, чтобы прицепить к конкретному родителю новый элемент. Такая команда должна инициализироваться идентификатором родителя, типом создаваемого объекта, его именем и возможно позицией, которую он занимает на экране при создании (актуально для объектов, вставляемых в Container).
Отмена такой команды заключается в удалении соответствующего объекта. Для нее необходим метод в модели — addElement(). Внутри него происходит идентификация типа объекта — Category или Container, и добавление соответствующей команды в стек (addElementToCategory, addElementToContainer).
3) Удаление элемента.
Намного более сложная ситуация с удалением элементов.
Дело в том, что у каждого элемента может существовать множество детей и сам элемент, как и его дети, может обладать различными связями. Для такого действия в модели должен быть создан метод deleteElement().
Удаляется элемент так: пробежать по дереву (корнем которого является сам удаляемый объект), освободить от связей детей, удалить детей, удалить сам элемент.
Заметим, что для освобождения детей от связей, можно воспользоваться уже существующей командой – ChangeRoleCommand, т.к. любая связь отражена в значении соответствующей ей роли. Удаление элемента в такой модели можно представить рекурсивно: чтобы удалить объект, нужно удалить его детей, а потом удалить его самого. Удаление детей происходит по такому же алгоритму. Так возникает идея использоваться каскадное удаление. То есть сделать одну команду, которая удаляет только один объект, освобождает его от связей, и делает все необходимые проверки, и реализовать удаление всего куска дерева на основе такой одной команды. При выполнении такой команды, считается, что у объекта нет детей, потому, что все его дети уже удаленны.
Покажем, как можно реализовать каскадной удаление (через обход дерева в глубину) используя стандартные возможности QUndoStack. Как упоминалось ранее, у QUndoStack есть два интересных метода, позволяющих делать собственные макросы из команд – beginMacro() и endMacro(). QUndoStack устроен так, что beginMacro() обозначает начало одной составной команд, а endMacro() ее конец. Между вызовом beginMacro() и endMacro() может выполняться какой угодно код, но любая команда, положенная в стек в этом промежутке, записывается в цепочку действий составной команды. Если на вершине стека находится составная команда, то при вызове undo(), у QUndoStack вызовется undo() пошагово - от последней команды из цепочки к первой. Вызов redo(), вызовет пошагово redo() у каждой команды в цепочки, начиная с первой и заканчивая последней.
Покажем, как будет выглядеть удаление вот такого объекта (A):


Снимок стека:


Реализация каскадного удаления в редакторе QReal описано в Приложении 2.

7. Заключение.
В рамках данной курсовой работы были достигнуты следующие результаты:


  1. Сформулирован подход к реализации undo/redo: пользовательские действия нужно разбивать на более мелкие и образовывать составные команды. Для работы с составными командами использовать такую стандартную возможность QUndoStack, как организация макросов методами beginMacro(), endMacro().




  1. Реализованы каскадные операции, такие как «удаление элемента»: удаление выполнено в виде макроса состоящего из простых команд – удалить отдельно взятый объект, изменить связанные с ним роли.




  1. Обнаружены дефекты существующей модели: не существует постоянных идентификаторов у объектов модели, поэтому после удаления, а затем восстановления объекта, нарушаются старые связи, зависящие от идентификатора – дети, идентификатор объекта в качестве значения роли и т.д.


В процессе выполнения курсовой работы автор изучил OC Linux, библиотеку Qt4, систему контроля версий (Subversion) и познакомился с некоторыми особенностями командной разработки ПО.
На основе обсуждений с разработчиками QReal и статей на эту тему, удалось сформировать подход к решению поставленной задачи и продемонстрировать его жизнеспособность, реализовав его на практике.

8. Приложения.
Приложение 1.
Пример реализации команды ChangeUserRole:
class ChangeRoleCommand : public UndoCommand

{

public:

ChangeRoleCommand (RealRepoModel *model, const QModelIndex& index, QVariant oldval,const QVariant& newval,int role);
virtual void undo();

virtual void redo();
protected:

RealRepoModel *model;

IdType repoId;

QVariant oldval;

QVariant newval;

int role;

};


ChangeRoleCommand::ChangeRoleCommand( RealRepoModel *model, const QModelIndex& index, QVariant oldval, const QVariant & newval, int role):

model(model), oldval(oldval), newval(newval), role(role)

{

RealRepoModel::RepoTreeItem *curItem = static_cast(index.internalPointer());

repoId = curItem->id;
}

void ChangeRoleCommand::undo()

{

int i;

for (i = 0; i < model->hashTreeItems[repoId].size(); i++)

if (model->type(model->hashTreeItems[repoId].at(i)->parent) == RealRepoModel::Container)

break;

if (i == model->hashTreeItems[repoId].size())

return;

RealRepoModel::RepoTreeItem* curItem = model->hashTreeItems[repoId].at(i);

QModelIndex ind = model->index(curItem);

model->setData(ind, oldval, role);

}

void ChangeRoleCommand::redo()

{

qDebug() << "beginRedo";

int i;

for (i = 0; i < model->hashTreeItems[repoId].size(); i++)

if (model->type(model->hashTreeItems[repoId].at(i)->parent) == RealRepoModel::Container)

break;

if (i == model->hashTreeItems[repoId].size())

return;

RealRepoModel::RepoTreeItem* curItem = model->hashTreeItems[repoId].at(i);

QModelIndex ind = model->index(curItem);

model->setData(ind, newval, role);

}
Приложение 2.
Метод deleteElement() в модели:
void RealRepoModel::deleteElement(QModelIndex index)

{

undoStack->beginMacro("Delete Element");
internalDeleteElement(index);
undoStack->endMacro();

}
void RealRepoModel::internalDeleteElement(QModelIndex ind)

{

RepoTreeItem *item = static_cast(ind.internalPointer());

foreach (RepoTreeItem *cur, item->children){

internalDeleteElement(index(cur));

}

undoStack->push(new DeleteElementCommand(this,ind));

}
Реализации команды DeleteElementCommand
DeleteElementCommand::DeleteElementCommand(RealRepoModel *model, QModelIndex index):

model(model)

{

setText("Delete Element");

RealRepoModel::RepoTreeItem *curItem = static_cast(index.internalPointer());

repoId = curItem->id;
type = model->hashTypes[repoId];
//сохраняем старые значение ролей

name = QVariant(model->hashNames[repoId]);

position = QVariant(model->hashDiagramElements[curItem->parent->id][curItem->id].position);

configuration = QVariant(model->hashDiagramElements[curItem->parent->id][curItem->id].configuration);
QStringList propertyNameList = model->info.getColumnNames(type);

for(int i = 0; i < propertyNameList.size();i++){

roleValues[model->info.roleByColumnName(type, propertyNameList.at(i))] = model->repoClient->getPropValue(repoId, propertyNameList.at(i));

}
}

void DeleteElementCommand::undo()

{

qDebug() << "undo deleteElement" << parentId;

int i;

for (i = 0; i < model->hashTreeItems[parentId].size(); i++)

{

qDebug() << "testing" << model->type(model->hashTreeItems[parentId].at(i)->parent) << RealRepoModel::Container;

if (model->type(model->hashTreeItems[parentId].at(i)->parent) == RealRepoModel::Container)

break;

}

if (i == model->hashTreeItems[parentId].size())

i--;

RealRepoModel::RepoTreeItem* parentItem = model->hashTreeItems[parentId].at(i);
QModelIndex parentIndex = model->index(parentItem);
model->beginInsertRows(parentIndex, parentItem->children.size(), parentItem->children.size());
qDebug() << "creating object with parent";

IdType id = model->repoClient->createObjectWithParent(type, name.toString(), parentId, repoId);
model->endInsertRows();
//возвращаем старые значения

RealRepoModel::RepoTreeItem* curItem = model->createItem(parentItem, repoId, type);

QModelIndex curIndex = model->index(curItem);
QMap::const_iterator it = roleValues.constBegin();

while (it != roleValues.constEnd()) {

model->setData(curIndex,it.value(),it.key());

++it;

}
//дописываем имя и позицию

model->setData(curIndex, name, Qt::EditRole);

model->setData(curIndex, position, Unreal::PositionRole);

model->setData(curIndex, configuration, Unreal::ConfigurationRole);

}

void DeleteElementCommand::redo()

{

qDebug() << "undo deleteElement";
//находим соответствущий ему RepoTreeItem

int i;

for (i = 0; i < model->hashTreeItems[repoId].size(); i++)

//хак, чтобы отличить аватар от оригинала

if (model->type(model->hashTreeItems[repoId].at(i)->parent) == RealRepoModel::Container)

break;

if (i == model->hashTreeItems[repoId].size())

return;

RealRepoModel::RepoTreeItem* curItem = model->hashTreeItems[repoId].at(i);
//получаем индекс в модели найденного RepoTreeItem

QModelIndex ind = model->index(curItem);
//получаем айдишник родителя

RealRepoModel::RepoTreeItem* parentItem = curItem->parent;

parentId = parentItem->id;
//начинаем удаление

model->beginRemoveRows(ind.parent(), ind.row(), ind.row());
QStringList l = model->info.getColumnNames(type);
qDebug() << "Processing referrals";

foreach(QString p, l)

{

// When deleting object, process refs first

if (model->info.isPropertyRef(type, p))

model->changeRole(ind, "", model->info.roleByColumnName(type, p));

}
model->repoClient->deleteObject(curItem->id, parentItem->id);
model->hashTreeItems[curItem->id].removeAll(parentItem->children.at(ind.row()));
delete parentItem->children.at(ind.row());

parentItem->children.removeAt(ind.row());
for ( int j = 0; j < parentItem->children.size(); j++ )

parentItem->children[j]->row = j;
model->endRemoveRows();

}

9. Список литературы.


  1. Model-View-Controller - http://en.wikipedia.org/wiki/Model-view-controller

  2. Брыксин Т.А., «Model/View-архитектура CASE-пакета REAL-MW», 2007 г.

  3. Undo commands - http://designinginterfaces.com/Multi-Level_Undo

  4. QUndoStack - http://doc.crossplatform.ru/qt/en/4.3.5/qundo.html

  5. Model/View features in Qt - http://doc.crossplatform.ru/qt/4.4.3/model-view-model.html

  6. Using Undo/Redo with Item Views - http://doc.trolltech.com/qq/qq25-undo.html

Добавить документ в свой блог или на сайт

Похожие:

Курсовая работа студента 342 группы Матвеева Н. Н. «Реализация отмены/повтора некоторых пользовательских действий в редакторе qreal» icon Положение о курсовой работе для преподавателей и студентов отделения «Теория музыки»
...
Курсовая работа студента 342 группы Матвеева Н. Н. «Реализация отмены/повтора некоторых пользовательских действий в редакторе qreal» icon Курсовая работа (проект) это документ, представляющий собой самостоятельную...
...
Курсовая работа студента 342 группы Матвеева Н. Н. «Реализация отмены/повтора некоторых пользовательских действий в редакторе qreal» icon Методические указания по выполнению курсового проекта
Выполнению работы предшествует всестороннее изучение теоретического и практического материала, отраженного в рекомендуемых к изучению...
Курсовая работа студента 342 группы Матвеева Н. Н. «Реализация отмены/повтора некоторых пользовательских действий в редакторе qreal» icon Курсовая работа По учебной дисциплине «Основы отраслевого менеджмента»
«Основы отраслевого менеджмента» Выдано студенту (студентке) Слободину Виталию группы 3302 12ПМ
Курсовая работа студента 342 группы Матвеева Н. Н. «Реализация отмены/повтора некоторых пользовательских действий в редакторе qreal» icon «Организация эвм» Контрольно курсовая работа «Проектирование вычислительной системы»
Данная контрольно-курсовая работа выполняется с целью закрепления знаний по курсу «Организация ЭВМ и систем» и получения практических...
Курсовая работа студента 342 группы Матвеева Н. Н. «Реализация отмены/повтора некоторых пользовательских действий в редакторе qreal» icon Курсовая работа или курсовой проект это задание, которое «выполняется...
Курсовая работа вид самостоятельной научно-методической работы студентов учебных заведений, которая выполняется под непосредственным...
Курсовая работа студента 342 группы Матвеева Н. Н. «Реализация отмены/повтора некоторых пользовательских действий в редакторе qreal» icon Курсовая работа по литературе студентки 232 группы филологического...
«Я знаю, что такое нигилизм, но никак не доберусь способа отделить настоящих нигилистов от шальных шавок, окричавших себя нигилистами....
Курсовая работа студента 342 группы Матвеева Н. Н. «Реализация отмены/повтора некоторых пользовательских действий в редакторе qreal» icon Курсовая работа является одним из этапов изучения курса "Аудит"
Допускаются. 14 января список группы с выбранной тематикой сдается менеджеру кафедры бухгалтерского учета и аудита для подготовки...
Курсовая работа студента 342 группы Матвеева Н. Н. «Реализация отмены/повтора некоторых пользовательских действий в редакторе qreal» icon Курсовая работа

Курсовая работа студента 342 группы Матвеева Н. Н. «Реализация отмены/повтора некоторых пользовательских действий в редакторе qreal» icon Взаимодействие свинца и некоторых других металлов с макрофитами
Цель работы – дать обзор некоторых данных о взаимодействии свинца и некоторых других металлов с водными макрофитами
Литература


При копировании материала укажите ссылку © 2015
контакты
literature-edu.ru
Поиск на сайте

Главная страница  Литература  Доклады  Рефераты  Курсовая работа  Лекции