Автор: Изюмов В.В.
MATLAB представляет собой мощную среду разработки и визуализации научных и инженерных расчетов, так же MATLAB имеет очень мощный инструмент GUIDE для разработки внутренних и внешних приложений. К сожалению, документация тулбокса GUIDE отражает элементарные приемы, и не описывает более глубокие аспекты создания приложений. Как, например, взаимодействие пользователя с графиками при помощи мыши. Так же в документации не было найдено описание работы приложений основанной на событиях.
Задача которая стала передо мной заключалась в следующем. Разработать приложение для создания и редактирования сигнала опираясь на шаблонный сигнал. То есть требовалось на одном графике выводить редактируемый и опорный сигнал и при помощи мыши устанавливать на графике маркеры редактирования, а также выделять отрезок опорного сигнала для проигрывания на аудиокарте.
Все приложения MATLAB созданые при помощи GUIDE имеют иерархическую структуру.
Рисунок 1 - Иерархия приложений MATLAB
На самом нижнем уровне находиться корневой объект класса root, представляющий собой визуально экран монитора, и хранит в себе его параметры, а так же точное положение курсора мыши, обновляемое при каждом изменении.
На следующем уровне находятся объекты класса figure
. Объектами класса figure
являются любое окно созданное MATLAB'ом, будь то график или приложение. Класс figure
содержит в себе: заголовки всех объектов находящиеся на нем, размер, расположение и другие характеристики окна, а также следит за действия производимые над объектами пользователем. Перечислим нужные для нас поля класса figure
.
CreateFcn, CloseRequestFcn
- загаловки функций вызываемых при создании и закрытии окна соответственно.
CurrentPoint
- позиция курсора мыши при последнем нажатии
Position
- позиция и размер окна
WindowButtonDownFcn
- заголовок функции вызываемой при нажатии кнопки мыши
WindowButtonMotionFcn
- заголовок функции вызываемой при перемещении мыши
WindowButtonUpFcn
- заголовок функции вызываемой при отпускании кнопки мыши
На третьем уровне иерархии находятся объекты классов axis
и uiconrol
. Данные объекты служат для управления окном и вывода нужной информации. Данные классы содержат важное для нас поле ButtonDownFcn
, которое содержит заголовок функции вызываемой при нажатии кнопки мыши на данном объекте.
Класс axis
на четвертом уровне содержит объекты отвечающие за отображение графических объектов, таких как: линии, поверхности, изображения и др.
Механизм обработки действий построен на четырех событиях, которые отслеживают действия мыши. Во время любого движения мыши, то есть изменения позиции курсора, вызывается функция заголовок которой записан в поле WindowButtonMotionFcn
класса figure
. При вызове данной функции обновляется поле CurrentPoint
в которой храниться позиция курсора.
Рисунок 2 - Пример появления событий от мыши
Когда пользователь нажимает какую либо клавишу мыши, сначала вызывается функция заголовок которой записан в поле ButtonDownFcn
объекта который находиться под указателем мыши. Стоит заметить что только в данной функции мы можем отследить двойное нажатие кнопки мыши. После этого вызывается функция записанная в поле WindowButtonDownFcn
класса figure
. Когда пользователь отпускает кнопку мыши вызывается функция записанная в поле WindowButtonUpFcn
. У данного события есть пару нюансов. События отжатия кнопки мыши возникает в том окне, в котором возникло событие нажатия клавиши мыши. Поэтому курсору не обязательно находиться над окном на котором произошло нажатие кнопки. Это следует учитывать когда требуется реализовать операцию Drag&Drop. Данную операцию перетаскивания надо обрабатывать приложением откуда происходит перетаскивание. Еще один нюанс заключается в том, что если в функциях WindowButtonDownFcn
, WindowButtonMotionFcn
происходит вызов функции drawnow
или функции содержащие drawnow
и при этом параметр Interruptible
равен off
, то событие WindowButtonUpFcn
может быть не отслеженна. Для устранения этого эффекта требуется установить параметр Interruptible
в значение off
, но при этом может возникнуть ситуация когда интервалы времени работы функций WindowButtonDownFcn
, WindowButtonMotionFcn
, WindowButtonUpFcn
могут пересечься, что может повлиять на работы приложения.
Определимся как должен реагировать наш график.
Когда с задачами определились, перейдем к реализации. (Далее будут приводиться отрывки из полного кода, который можно посмотреть в конце)
Для начала создадим необходимые нам элементы управления: 2 линии маркеров (M1,M2
), 2 линии границ выделения (S1,S2
) и прямоугольник для отображения выделения (Slc
). Для этого внесем в функцию инициализации соответствующие функции создания элементов. Так же нам потребуется две переменные: butPress
- для индикации нажата клавиша мыши (butPress='Down'
) или нет (butPress='Up'
), и object
- для запоминания объекта графика на котором была нажата клавиша. А так же назначим функции для обработки нажатия клавиши мыши.
function segtool_OpeningFcn(hObject, eventdata, handles, varargin) ... patch([-1; -1; 1; 1],[-2; 2; 2; -2],[-1;-1;-1;-1],'r',... 'Tag','Slc',... 'FaceColor',[0.5 0.5 1],... 'FaceAlpha',0.6,... 'Visible','off',... 'ButtonDownFcn','segtool(''axes1_ButtonDownFcn'',gcbo,[],guidata(gcbo))'); line([-1 -1],[-2 2],'Tag','S1',... 'LineStyle',':',... 'Visible','off',... 'ButtonDownFcn','segtool(''line_ButtonDownFcn'',gcbo,[],guidata(gcbo))'); line([1 1],[-2 2],'Tag','S2',... 'LineStyle',':',... 'Visible','off',... 'ButtonDownFcn','segtool(''line_ButtonDownFcn'',gcbo,[],guidata(gcbo))'); line([0 0],[-2 2],'Tag','M1',... 'Color','g',... 'LineStyle',':',... 'LineWidth',1,... 'Visible','off',... 'ButtonDownFcn','segtool(''line_ButtonDownFcn'',gcbo,[],guidata(gcbo))'); line([0 0],[-2 2],'Tag','M2',... 'Color','g',... 'LineStyle',':',... 'LineWidth',1,... 'Visible','off',... 'ButtonDownFcn','segtool(''line_ButtonDownFcn'',gcbo,[],guidata(gcbo))'); handles.butPress='Up'; handles.object='none'; % Update handles structure guidata(hObject, handles);
Теперь нам надо сконструировать механизм обработки событий генерируемые мышью. Наш механизм должен разделен на три этапа, которые обрабатывают соответствующие события от мыши: нажатие, отжатие кнопки и передвижение мыши. Для первого этапа потребуется две функции: первая для создания маркеров и выделения, вторая - для редактирования положения маркеров и границ выделения.
Первая функция обработки нажатия клавиши. Назначается графику, и области выделения (что-бы область выделения не мешала устанавливать маркеры)
function axes1_ButtonDownFcn(hObject, eventdata, handles) butType=get(handles.figure1,'SelectionType'); Pos=get(handles.axes1,'Position'); XY=xyinaxes(get(handles.figure1,'CurrentPoint'),Pos); switch butType case 'normal' % LMB if isequal('off',get(findobj('Tag','M2'),'Visible')) handles.butPress='Down'; h=findobj('Tag','M1'); if isequal('on',get(h,'Visible')) h=findobj('Tag','M2'); end x=round(pos2x(XY(1),get(handles.axes1,'XLim'),Pos(3))); set(h,'XData',[x x],'Visible','on'); handles.object=get(h,'Tag'); end case 'extend' % Shift+LMB handles.butPress='Down'; x=round(pos2x(XY(1),get(handles.axes1,'XLim'),Pos(3))); set(findobj('Tag','S1'),'XData',[x x],'Visible','on'); set(findobj('Tag','S2'),'XData',[x x],'Visible','on'); handles.object='S2'; set(findobj('Tag','Slc'),'XData',[x; x; x; x],'Visible','on'); end guidata(hObject,handles);
Вторая функция обработки нажатия клавиши мыши. Назначается линиям маркеров и границам выделения.
function line_ButtonDownFcn(hObject, eventdata, handles) butType=get(handles.figure1,'SelectionType'); switch butType case 'normal' handles.butPress='Down'; handles.object=get(hObject,'Tag'); case 'alt' switch get(hObject,'Tag') case 'M1' h=findobj('Tag','M2'); if isequal(get(h,'Visible'),'on') set(h,'Tag','M1'); set(hObject,'Visible','off','Tag','M2'); else set(hObject,'Visible','off'); end case 'M2' set(hObject,'Visible','off'); end end guidata(hObject,handles);
Функция обработки движения мыши. Передвигает соответствующие объекты и если объектом является граница выделения, то корректирует прямоугольник, который отображает область выделение.
function figure1_WindowButtonMotionFcn(hObject, eventdata, handles) pos=get(handles.axes1,'Position'); XY=xyinaxes(get(handles.figure1,'CurrentPoint'),pos); if ((XY(1)>=0)&(XY(1)<=pos(3)))&((XY(2)>=0)&(XY(2)<=pos(4))) if isequal(handles.butPress,'Down') x=round(pos2x(XY(1),get(handles.axes1,'XLim'),pos(3))); set(findobj('Tag',handles.object),'XData',[x x]); if isequal(handles.object,'S1')||isequal(handles.object,'S2') h=findobj('Tag','Slc'); px=get(h,'XData'); switch handles.object case 'S1' px(1:2)=x; case 'S2' px(3:4)=x; end set(h,'XData',px); end end end
Заключительный, третий этап обработки сообщений генерируемых мышью. Задача данной функции, чтобы всегда первый маркер был слева, а второй справа. Так же и для границ области выделения. Данная сортировка позволила значительно уменьшить код выше приведенных функций.
function figure1_WindowButtonUpFcn(hObject, eventdata, handles) pos=get(handles.axes1,'Position'); XY=xyinaxes(get(handles.figure1,'CurrentPoint'),pos); if isequal(handles.butPress,'Down') switch handles.object case {'M1','M2'} hm2=findobj('Tag','M2'); if isequal(get(hm2,'Visible'),'on') hm1=findobj('Tag','M1'); xm1=get(hm1,'XData'); xm2=get(hm2,'XData'); if xm1(1)>xm2(1) set(hm1,'Tag','M2'); set(hm2,'Tag','M1'); end end case {'S1','S2'} hs1=findobj('Tag','S1'); hs2=findobj('Tag','S2'); xs1=get(hs1,'XData'); xs2=get(hs2,'XData'); if xs1(1)>xs2(1) set(hs1,'Tag','S2'); set(hs2,'Tag','S1'); set(findobj('Tag','Slc'),... 'XData',[xs2(1);xs2(1);xs1(1);xs1(1)]); end end end handles.butPress='Up'; guidata(hObject,handles);
Как видно из выше приведенного кода, механизм реализующий интерактивную работу с объектами на графике при помощи мыши достаточно прост. И реализация с помощью GUIDE, по сравнению с другими методами, сокращает время разработки данного механизма. Операции с графиком, такие как: масштабирование и перемение, реализуются намного проще, поэтому здесь не описываются, но их можно посмотреть в полном коде приложения, реализующего задачу приведенную в начале.
(Полный код приложения: файл описания окна - segtool.fig, сам код - segtool.m)