MATLAB GUI的运行原理理解

2022-09-01 11:12:13 浏览数 (1)

大家好,又见面了,我是你们的朋友全栈君。

背景

为了在MATLAB上演示算法,最近学了一下MATLAB的GUI,学习方法就是一边用一边百度……由于用这种随意的学习方法,这个过程中我也是遇到各种问题,花了不少精力。为了使得这些痛苦的过程不被浪费,这里就总结一下使用MATLAB GUI的知识点,已便今后查阅。

MATLAB GUI的运行原理——创建窗口

在保存GUI的fig文件后,MATLAB会自动生成一个函数,函数名与fig文件名相同。这个函数就是GUI的入口。可以看到个m文件底下自动生成了两个函数:

  • [GUI名]_OpeningFcn
  • [GUI名]_OutputFcn

其中OpeningFcn在程序开始后,界面出来前被运行,我会在这里面执行一些初始化操作。OutputFcn在界面出现后被执行,这个函数返回的值会被作为入口函数的返回值输出。

MATLAB的GUI其实很容易使用,完全按照回调函数的思想去写代码就可以,但是这样往往会遇到一些无法理解的错误,所以我尝试对MATLAB GUI的原理进行了一些研究,看过这篇文章后我在GUI的入口函数设了一个断点,并单步运行,粗略地浏览了一下代码,虽说还有很多地方不理解,但是对GUI的运行方式大体有了一个概念。了解MATLAB GUI的运行原理对以后的debug有好处,所以写下来做总结以及记录。 在入口函数出设断点,开始单步调试,这时函数式没有参数的。可以看到程序最后会调用一个叫gui_mainfcn的函数。 步进这个函数,第一段代码如下

代码语言:javascript复制
gui_StateFields =  { 
   'gui_Name' 'gui_Singleton' 'gui_OpeningFcn' 'gui_OutputFcn' 'gui_LayoutFcn' 'gui_Callback'};
gui_Mfile = '';

% 获取文件名
for i=1:length(gui_StateFields)
    if ~isfield(gui_State, gui_StateFields{i})
        error(message('MATLAB:guide:StateFieldNotFound', gui_StateFields{ i }, gui_Mfile));
    elseif isequal(gui_StateFields{i}, 'gui_Name')
        gui_Mfile = [gui_State.(gui_StateFields{i}), '.m'];
    end
end

这里获取了GUI项目的文件名,但我往下面看没有发现gui_Mfile被使用,所以认为段代码的作用是检查参数的正确性。

代码语言:javascript复制
numargin = length(varargin);

if numargin == 0
    % UNTITLED
    % create the GUI only if we are not in the process of loading it
    % already
    gui_Create = true;
elseif local_isInvokeActiveXCallback(gui_State, varargin{:})
    % UNTITLED(ACTIVEX,...)
    vin{
  
  1} = gui_State.gui_Name;
    vin{
  
  2} = [get(varargin{
  
  1}.Peer, 'Tag'), '_', varargin{end}];
    vin{
  
  3} = varargin{
  
  1};
    vin{
  
  4} = varargin{end-1};
    vin{
  
  5} = guidata(varargin{
  
  1}.Peer);
    feval(vin{:});
    return;
elseif local_isInvokeHGCallback(gui_State, varargin{:})
    % UNTITLED('CALLBACK',hObject,eventData,handles,...)
    gui_Create = false;
else
    % UNTITLED(...)
    % create the GUI and hand varargin to the openingfcn
    gui_Create = true;
end

这里的varargin是从GUI入口函数的参数传入的,上面说到入口函数没有 参数,所以numargin 应该等于0,使得gui_Create被设为真。根据注释其实可以猜测这里gui_Create是一个决定了是否创建GUI的标志位。那为什么需要判断这样一个标志位呢?其实通过注释可以猜测,gui_mainfcn这个函数不但负责建立GUI,同时还负责响应来自各个控件的Callback,但是我们姑且不管这个猜测的正确性。

代码语言:javascript复制
if ~gui_Create
    ... %省略中间的代码
else
    if gui_State.gui_Singleton
        gui_SingletonOpt = 'reuse';
    else
        gui_SingletonOpt = 'new';
    end

    % Check user passing 'visible' P/V pair first so that its value can be
    % used by oepnfig to prevent flickering
    gui_Visible = 'auto';
    gui_VisibleInput = '';
    for index=1:2:length(varargin)
        if length(varargin) == index || ~ischar(varargin{index})
            break;
        end

        % Recognize 'visible' P/V pair
        len1 = min(length('visible'),length(varargin{index}));
        len2 = min(length('off'),length(varargin{index 1}));
        if ischar(varargin{index 1}) && strncmpi(varargin{index},'visible',len1) && len2 > 1
            if strncmpi(varargin{index 1},'off',len2)
                gui_Visible = 'invisible';
                gui_VisibleInput = 'off';
            elseif strncmpi(varargin{index 1},'on',len2)
                gui_Visible = 'visible';
                gui_VisibleInput = 'on';
            end
        end
    end
    ...%省略下面代码,这些代码在下面分块分析
end
%函数返回

由于gui_Create为真,这里if语句的判断结果肯定为假。然后程序首先判断了窗口的Singleton性质和可见性。由于这里程序太长,下面再分块分析。

代码语言:javascript复制
    % Do feval on layout code in m-file if it exists
    gui_Exported = ~isempty(gui_State.gui_LayoutFcn);
    % this application data is used to indicate the running mode of a GUIDE
    % GUI to distinguish it from the design mode of the GUI in GUIDE. it is
    % only used by actxproxy at this time.   
    %直接把数据保存在UI中
    setappdata(0,genvarname(['OpenGuiWhenRunning_', gui_State.gui_Name]),1);
    if gui_Exported
        gui_hFigure = feval(gui_State.gui_LayoutFcn, gui_SingletonOpt);

        % make figure invisible here so that the visibility of figure is
        % consistent in OpeningFcn in the exported GUI case
        if isempty(gui_VisibleInput)
            gui_VisibleInput = get(gui_hFigure,'Visible');
        end
        set(gui_hFigure,'Visible','off')

        % openfig (called by local_openfig below) does this for guis without
        % the LayoutFcn. Be sure to do it here so guis show up on screen.
        movegui(gui_hFigure,'onscreen');
    else
        gui_hFigure = local_openfig(gui_State.gui_Name, gui_SingletonOpt, gui_Visible);
        % If the figure has InGUIInitialization it was not completely created
        % on the last pass.  Delete this handle and try again.
        if isappdata(gui_hFigure, 'InGUIInitialization')
            delete(gui_hFigure);
            gui_hFigure = local_openfig(gui_State.gui_Name, gui_SingletonOpt, gui_Visible);
        end
    end
    if isappdata(0, genvarname(['OpenGuiWhenRunning_', gui_State.gui_Name]))
%       移除相应的变量
        rmappdata(0,genvarname(['OpenGuiWhenRunning_', gui_State.gui_Name]));
    end

这段语句判断了输入的参数是否存在回调函数gui_LayoutFcn,如果存在就用这个回调函数来创建窗口gui_hFigure,否则就通过函数local_openfig创建一个窗口,并且设置相应的可见性。一般运行到这里可见性都会被设为’off’,或者’auto’,通过继续追踪local_openfig函数,可以发现可见性与窗口的Singleton性质有关,但如果Singleton性质为假,则仍是不可见的。也就是说,在创建GUI的时候,到这里界面仍然不会出来。

代码语言:javascript复制
    % Set flag to indicate starting GUI initialization
    setappdata(gui_hFigure,'InGUIInitialization',1);

    % Fetch GUIDE Application options
    gui_Options = getappdata(gui_hFigure,'GUIDEOptions');
    % Singleton setting in the GUI M-file takes priority if different
    gui_Options.singleton = gui_State.gui_Singleton;

    if ~isappdata(gui_hFigure,'GUIOnScreen')
        % Adjust background color
        if gui_Options.syscolorfig
            set(gui_hFigure,'Color', get(0,'DefaultUicontrolBackgroundColor'));
        end

        % Generate HANDLES structure and store with GUIDATA. If there is
        % user set GUI data already, keep that also.
        data = guidata(gui_hFigure);
        handles = guihandles(gui_hFigure);
        if ~isempty(handles)
            if isempty(data)
                data = handles;
            else
                names = fieldnames(handles);
                for k=1:length(names)
                    data.(char(names(k)))=handles.(char(names(k)));
                end
            end
        end
        guidata(gui_hFigure, data);
    end

这里对窗口的背景色和handles进行了初始化,使用过MATLAB GUI的话可以知道,handles是GUI中的一个与窗口绑定的结构体,用来保存各控件对象和用户数据。

代码语言:javascript复制
% Apply input P/V pairs other than 'visible'
    for index=1:2:length(varargin)
        if length(varargin) == index || ~ischar(varargin{index})
            break;
        end

        len1 = min(length('visible'),length(varargin{index}));
        if ~strncmpi(varargin{index},'visible',len1)
            try set(gui_hFigure, varargin{index}, varargin{index 1}), catch break, end
        end
    end

    % If handle visibility is set to 'callback', turn it on until finished
    % with OpeningFcn
    gui_HandleVisibility = get(gui_hFigure,'HandleVisibility');
    if strcmp(gui_HandleVisibility, 'callback')
        set(gui_hFigure,'HandleVisibility', 'on');
    end

%    开始运行gui_OpeningFcn
    feval(gui_State.gui_OpeningFcn, gui_hFigure, [], guidata(gui_hFigure), varargin{:});

这里先设置了窗口的句柄的可见性,然后开始运行GUI函数m文件下自动生成的一个子函数gui_OpeningFcn。所以从这里可以看出,这个函数是GUI中第一个被调用的函数,且在GUI显示之前就执行了。

代码语言:javascript复制
    if isscalar(gui_hFigure) && ishghandle(gui_hFigure)
        % Handle the default callbacks of predefined toolbar tools in this
        % GUI, if any
        guidemfile('restoreToolbarToolPredefinedCallback',gui_hFigure); 

        % Update handle visibility
        set(gui_hFigure,'HandleVisibility', gui_HandleVisibility);

        % Call openfig again to pick up the saved visibility or apply the
        % one passed in from the P/V pairs
%         设置fig的可见性?
        if ~gui_Exported
            gui_hFigure = local_openfig(gui_State.gui_Name, 'reuse',gui_Visible);
        elseif ~isempty(gui_VisibleInput)
            set(gui_hFigure,'Visible',gui_VisibleInput);
        end
        if strcmpi(get(gui_hFigure, 'Visible'), 'on')
            figure(gui_hFigure);

            if gui_Options.singleton
                setappdata(gui_hFigure,'GUIOnScreen', 1);
            end
        end

        % Done with GUI initialization
        if isappdata(gui_hFigure,'InGUIInitialization')
            rmappdata(gui_hFigure,'InGUIInitialization');
        end

经过这里窗口被设为了可见。可以看到local_openfig函数这是第二次出现了,上一次出现这个函数时窗口并没有可见,这一次却被设为了可见,我没有仔细研究这个函数里面的原理,因为我认为不需要研究它内部的原理就能够大致理解整个GUI的运行方式了,所以没有花再多的时间。

代码语言:javascript复制
  % If handle visibility is set to 'callback', turn it on until
        % finished with OutputFcn
        gui_HandleVisibility = get(gui_hFigure,'HandleVisibility');
        if strcmp(gui_HandleVisibility, 'callback')
            set(gui_hFigure,'HandleVisibility', 'on');
        end
        gui_Handles = guidata(gui_hFigure);
    else
        gui_Handles = [];
    end

    if nargout
        [varargout{1:nargout}] = feval(gui_State.gui_OutputFcn, gui_hFigure, [], gui_Handles);
    else
        feval(gui_State.gui_OutputFcn, gui_hFigure, [], gui_Handles);
    end

    if isscalar(gui_hFigure) && ishghandle(gui_hFigure)
        set(gui_hFigure,'HandleVisibility', gui_HandleVisibility);
    end

最后这里设置了下窗口句柄的可见性,重点是并且调用了gui_OutputFcn。gui_mainfcn函数运行到这里就返回了。

MATLAB GUI的运行原理——回调函数

当在GUI中加入控件后,可以引入回调函数,回调函数也就是等使用者按下相应按键时执行的函数。回调函数的调用看起来很神奇,其实很容易理解。在窗口创建起来后,在GUI入口函数处和回调函数处设置一个断点,然后点击一个按钮,你会发现程序会在这个断点处停下来,而不会直接去到回调函数。一开始我觉得很奇怪,为什么程序会莫名其妙地跑到了入口函数那里去,后面无意看到GUI控件回调函数的格式

@(hObject,eventdata)guitest(‘start_preview_Callback’,hObject,eventdata,guidata(hObject))

其中,这里guitest就是我的入口函数名,显然,这里是把回调函数以参数的形式传递给了入口函数,一并传递过去的还有当前控件的句柄。 通过单步调试可以发现程序仍然步进了gui_mainfcn函数,但是此时的参数与先前的不一样了,程序把入口函数接受的参数传送给该函数。步进gui_mainfcn函数后,如上文所述,程序会先判断参数的类型,发现该参数类型是回调函数的话,将gui_Create设为假,程序就不会再建立一个窗口。

代码语言:javascript复制
if ~gui_Create
    % In design time, we need to mark all components possibly created in
    % the coming callback evaluation as non-serializable. This way, they
    % will not be brought into GUIDE and not be saved in the figure file
    % when running/saving the GUI from GUIDE.
    designEval = false;
    if (numargin>1 && ishghandle(varargin{
  
  2}))
        fig = varargin{
  
  2};
        while ~isempty(fig) && ~ishghandle(fig,'figure')
            fig = get(fig,'parent');
        end

        designEval = isappdata(0,'CreatingGUIDEFigure') || (isscalar(fig)&&isprop(fig,'GUIDEFigure'));
    end

    if designEval
        beforeChildren = findall(fig);
    end

    % evaluate the callback now
    varargin{
  
  1} = gui_State.gui_Callback;
    if nargout
        [varargout{
  
  1:nargout}] = feval(varargin{:});
    else       
        feval(varargin{:});
    end

    % Set serializable of objects created in the above callback to off in
    % design time. Need to check whether figure handle is still valid in
    % case the figure is deleted during the callback dispatching.
    if designEval && ishghandle(fig)
        set(setdiff(findall(fig),beforeChildren), 'Serializable','off');
    end
else
...
end
%程序返回

程序先是获取了调用回调函数的源控件的顶级父容器,也就是窗口。然后通过函数句柄调用了相应的回调函数,在接收到回调函数的返回值后,把它送回给入口函数,再由入口函数进行输出。

后记

分析到这里,虽然整个程序之中还有一些一笔带过的地方,但是不阻碍对整个GUI大体流程的认识。在MATLAB的GUI中,我还有一个比较好奇的点是回调函数的中断,在GUI中当一个回调函数没有执行完毕另一个回调函数就已经被触发的时候,根据设置,如果前一个回调函数中有drawnow、uiwait等函数,在执行这些函数的时候前一个回调函数会被中断,然后程序会开始执行另一个回调函数,而前一个回调函数会被暂停,但不会中止,也就是说后面被触发的回调函数执行完毕后,前一个回调函数会在被打断的地方继续执行。

发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/143761.html原文链接:https://javaforall.cn

0 人点赞