Создание приложения в среде MATLAB 7.0 с помощью GUIDE: интерактивная работа с графиками при помощи мыши

Автор: Изюмов В.В.

MATLAB представляет собой мощную среду разработки и визуализации научных и инженерных расчетов, так же MATLAB имеет очень мощный инструмент GUIDE для разработки внутренних и внешних приложений. К сожалению, документация тулбокса GUIDE отражает элементарные приемы, и не описывает более глубокие аспекты создания приложений. Как, например, взаимодействие пользователя с графиками при помощи мыши. Так же в документации не было найдено описание работы приложений основанной на событиях.

Задача которая стала передо мной заключалась в следующем. Разработать приложение для создания и редактирования сигнала опираясь на шаблонный сигнал. То есть требовалось на одном графике выводить редактируемый и опорный сигнал и при помощи мыши устанавливать на графике маркеры редактирования, а также выделять отрезок опорного сигнала для проигрывания на аудиокарте.

Немного теории для реализации данной задачи.

Все приложения MATLAB созданые при помощи GUIDE имеют иерархическую структуру.

иерархия приложения MATLAB
Рисунок 1 - Иерархия приложений MATLAB

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

На следующем уровне находятся объекты класса figure. Объектами класса figure являются любое окно созданное MATLAB'ом, будь то график или приложение. Класс figure содержит в себе: заголовки всех объектов находящиеся на нем, размер, расположение и другие характеристики окна, а также следит за действия производимые над объектами пользователем. Перечислим нужные для нас поля класса figure.

CreateFcn, CloseRequestFcn - загаловки функций вызываемых при создании и закрытии окна соответственно.

CurrentPoint - позиция курсора мыши при последнем нажатии

Position - позиция и размер окна

WindowButtonDownFcn - заголовок функции вызываемой при нажатии кнопки мыши

WindowButtonMotionFcn - заголовок функции вызываемой при перемещении мыши

WindowButtonUpFcn - заголовок функции вызываемой при отпускании кнопки мыши

На третьем уровне иерархии находятся объекты классов axis и uiconrol. Данные объекты служат для управления окном и вывода нужной информации. Данные классы содержат важное для нас поле ButtonDownFcn, которое содержит заголовок функции вызываемой при нажатии кнопки мыши на данном объекте.

Класс axis на четвертом уровне содержит объекты отвечающие за отображение графических объектов, таких как: линии, поверхности, изображения и др.

Механизм слежения за действиями мыши.

Механизм обработки действий построен на четырех событиях, которые отслеживают действия мыши. Во время любого движения мыши, то есть изменения позиции курсора, вызывается функция заголовок которой записан в поле WindowButtonMotionFcn класса figure. При вызове данной функции обновляется поле CurrentPoint в которой храниться позиция курсора.

Пример появления событий от мыши 
Gif-анимация: 26 кадров, 10 повторов
Рисунок 2 - Пример появления событий от мыши

Когда пользователь нажимает какую либо клавишу мыши, сначала вызывается функция заголовок которой записан в поле ButtonDownFcn объекта который находиться под указателем мыши. Стоит заметить что только в данной функции мы можем отследить двойное нажатие кнопки мыши. После этого вызывается функция записанная в поле WindowButtonDownFcn класса figure. Когда пользователь отпускает кнопку мыши вызывается функция записанная в поле WindowButtonUpFcn. У данного события есть пару нюансов. События отжатия кнопки мыши возникает в том окне, в котором возникло событие нажатия клавиши мыши. Поэтому курсору не обязательно находиться над окном на котором произошло нажатие кнопки. Это следует учитывать когда требуется реализовать операцию Drag&Drop. Данную операцию перетаскивания надо обрабатывать приложением откуда происходит перетаскивание. Еще один нюанс заключается в том, что если в функциях WindowButtonDownFcn, WindowButtonMotionFcn происходит вызов функции drawnow или функции содержащие drawnow и при этом параметр Interruptible равен off, то событие WindowButtonUpFcn может быть не отслеженна. Для устранения этого эффекта требуется установить параметр Interruptible в значение off, но при этом может возникнуть ситуация когда интервалы времени работы функций WindowButtonDownFcn, WindowButtonMotionFcn, WindowButtonUpFcn могут пересечься, что может повлиять на работы приложения.

От теории к практике.

Определимся как должен реагировать наш график.

  1. при нажатии левой клавиши в любом месте графика, должен устанавливаться маркер. На графике может быть максимум два маркера. (В целях, вначале приведенной, задачи при одном марке сигнал редактируется в точке маркера, а при двух маркерах - редактируется в пределах установленных маркерами);
  2. при удерживании левой кнопки мыши над маркером осуществлять его передвижение;
  3. при нажатии правой кнопки мыши над маркером производить его удалении;
  4. при помощи комбинации shift и левой кнопки мыши определять диапазон выделения;
  5. как в п.2 осуществлять передвижение границ выделения.

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

Для начала создадим необходимые нам элементы управления: 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)