Matlab中实现在Axes中直接操纵数据点

最新在用matlab写控制空间光调制器的程序,用来对脉冲激光器做色散补偿。由于激光不同波长色散函数不太规则,需要一个高度可调的曲线。为了方便,一个自然的想法就是在坐标区生成几个点然后鼠标拖着走,然而目前版本matlab(2020a)的app designer并没提供这么个控件,在matlab论坛上搜到了个 实现这个的方法 ,实现起来效果还不错,这里总结一下写一写。


代码的运行效果是这样的。

程序运行效果 https://www.zhihu.com/video/1234626772194168832

该app启动时回调函数如下

function startupFcn(app)
    % patch_handle用于存放被拖拽点的图形对象
    global patch_handle;
    % x,y 为app.pointNumber个控制点初始位置
    x = linspace(1,1920,app.pointNumber);
    y = ones(app.pointNumber,1)*0.5;
    % 给定坐标区的坐标范围,防止拖拽时候auto scale
    app.UIAxes.XLim = [1 1920];
    app.UIAxes.YLim = [0,1];
    % app.pointPosition用于存放控制点的坐标
    app.pointPosition = [x,y];
    % app.plotHandle用于存放差值曲线plot出来的line对象
    app.plotHandle = plot(app.UIAxes,1:1920,ones(1,1920)*0.5);
    hold(app.UIAxes,'on');
    % 利用只带一个坐标数据的patch对象生成控制点,并将其对象存入global变量patch_handle
    for i = numel(x):-1:1
            temp(i) = patch(app.UIAxes,'xdata',x(i),'ydata',y(i),...
                'linestyle','none','facecolor','none',...
                'marker','o','markerEdgecolor','k',...
                'buttonDownFcn',@app.drag,'userdata',i);
    patch_handle = temp;
end

这里需要注意的是,在生成控制点时,在其属性中加入了 buttonDownFcn 这个事件函数,并将其链接至在app中定义的私有函数drag,用于完成点的drag操作;除此之外,我们还为patch对象的属性 userdata 赋值,用于区分控制点。当matlab检测到其中某个点被鼠标按下,便会自动调用函数drag,并自动将 app patch对象 以及 鼠标点击下去的这个事件 ,这三个对象传递到函数drag,该函数代码如下:

function drag(app,src,~)
    app.currentPoint = get(src,'userdata');
    app.figureStatus = get(app.UIFigure,{'WindowButtonMotionFcn','WindowButtonUpFcn'});
    set(app.UIFigure,'WindowButtonMotionFcn',@app.move,'WindowButtonUpFcn',@app.drop);
end

drag函数作为app私有的事件函数,matlab强制要求其接受三个参数,第一个为app本身的对象,第二个为链接至它的图形对象,第三个为事件对象。由于该函数的函数体中,我们用不到调用事件对象,故没给他分配局域变量,只用了个占位符~放在那。相信不少人好奇那到底是个啥东西,我们可以在代码里面把函数改成drag(app, src, a),不加分号输出一下,如下

可以看到,这个对象名叫Hit,里面记录了按下了鼠标哪个键,在什么位置以及这个事件的名称Hit。

在drag函数里,我们首先把当前选中的那个点的编号给到app的属性currentPoint里。接下来是重头戏,由于matlab的UIAxes对象目前没有任何回调函数可用,于是在鼠标点下选中控制点后,我们借助UIFigure这个父对象提供的事件回调,WindowButtonMotionFcn以及WindowButtonUpFcn,前者对应于鼠标指针移动事件,后者则对应按键释放。我们将这两个事件分别连接到回调函数 move 以及 drop 中,由于是在app内定义的函数,故函数的handle要加上类名。

move函数定义如下

function move(app,~,~)
    global patch_handle;
    cursorPosition = app.UIAxes.CurrentPoint;
    % 更新坐标位置,鼠标拖出坐标区后让点别跟着跑出去
    xl = app.UIAxes.XLim;
    yl = app.UIAxes.YLim;
    xn = min(max(cursorPosition(1),xl(1)),xl(2));
    yn = min(max(cursorPosition(3),yl(1)),yl(2));
    % 更新所选中patch的顶点
    patch_handle(app.currentPoint).Vertices = [xn yn];
end

为了讲清楚,这里再啰嗦几句:我们的程序之所以能够进入这个move,是因为Matlab发现我们点击了patch对象,该事件的发生调用了函数drag,进而给UIFigure中鼠标移动的事件规定了一个响应方法,即move。函数 move 做的事情很简单,首先利用 UIAxes 的 CurrentPoint 属性读出当前鼠标在坐标区中的位置,再给我们选中的patch对象更新顶点属性即可,这里代码中 patch_handle(app.currentPoint) 用到了我们在drag函数中存进去的标识符(属性的名字命的不好,和UIAxes的属性重了。。懒得改就这样吧)。

至此,当我们点下patch后,通过drag函数引入的move函数,点就能随着我们的指针随时更新位置。如果只这样就结束了,那无论我们再做啥操作,那个patch都会一直跟着鼠标指针跑。故,为drag的 WindowButtonUpFcn 即按键释放提供的回调函数drop十分重要。其代码如下

function drop(app,~,~)
    temp = app.figureStatus;
    set(app.UIFigure,'WindowButtonMotionFcn',temp{1},'WindowButtonUpFcn',temp{2});
    app.showInterp();
end

这个函数做的事情也非常简单,drag函数中,给 UIFigure 声明事件响应前,我们先将那俩属性暂存到figureStatus这个app属性中了。在鼠标释放后,drop函数将 UIFigure 对于鼠标移动以及鼠标释放的响应清空,这样一来,点便不会再跟着指针跑。

drop函数最后一行用于更新根据控制点插值生成的曲线,函数如下

function showInterp(app)
    global patch_handle;
    data = app.pointPosition;
    for i = 1:length(data)
        data(i,:) = patch_handle(i).Vertices;