如何正确地组织GUI代码?

29

我正在为MATLAB Compiler开发一个相当复杂的GUI程序。(使用MATLAB构建此GUI的原因很好,但这不是本问题的重点。我意识到这种语言构建GUI并不是它的强项。)

有许多方法可以在GUI功能之间共享数据,甚至可以在应用程序中的GUI之间传递数据:

  • setappdata/getappdata/_____appdata - 将任意数据关联到一个句柄上
  • guidata - 通常与GUIDE一起使用;将GUI数据“存储或检索”到句柄结构中
  • 对句柄对象的UserData属性执行set/get操作
  • 在主函数中使用嵌套函数;基本上模拟了“全局”作用域变量。
  • 在子函数之间来回传递数据

我的代码结构并不是最美观的。现在,我的引擎与前端分离(很好!),但GUI代码非常像意面一样杂乱无章。以下是一个“活动”的框架,借鉴Android说法:

function myGui

    fig = figure(...); 

    % h is a struct that contains handles to all the ui objects to be instantiated. My convention is to have the first field be the uicontrol type I'm instantiating. See draw_gui nested function

    h = struct([]);


    draw_gui;
    set_callbacks; % Basically a bunch of set(h.(...), 'Callback', @(src, event) callback) calls would occur here

    %% DRAW FUNCTIONS

    function draw_gui
        h.Panel.Panel1 = uipanel(...
            'Parent', fig, ...
            ...);

        h.Panel.Panel2 = uipanel(...
            'Parent', fig, ...
            ...);


        draw_panel1;
        draw_panel2;

        function draw_panel1
             h.Edit.Panel1.thing1 = uicontrol('Parent', h.Panel.Panel1, ...);
        end
        function draw_panel2
             h.Edit.Panel2.thing1 = uicontrol('Parent', h.Panel.Panel2, ...);
        end


    end

    %% CALLBACK FUNCTIONS
    % Setting/getting application data is done by set/getappdata(fig, 'Foo').
end

我之前写的代码没有嵌套,因此我不得不在各处传递h(因为需要重新绘制、更新等操作),并使用setappdata(fig)存储实际数据。无论如何,我一直将一个“activity”保存在单个文件中,我确信这将成为未来维护的噩梦。回调函数与应用程序数据和图形句柄对象交互,我想这是必要的,但这会阻止代码基础的两个“半部分”完全分离。

因此,我正在寻找一些组织/ GUI设计帮助。具体包括:

  • 我应该使用哪种目录结构来进行组织?(回调 vs 绘图函数?)
  • 与GUI数据交互并将其与应用程序数据隔离的“正确方法”是什么?(当我提到GUI数据时,我指的是设置/获取句柄对象的属性)。
  • 如何避免将所有这些绘图函数放入数千行的巨大文件中,并有效地来回传递应用程序和GUI数据?这可行吗?
  • 频繁使用set/getappdata是否会有任何性能损失?
  • 我的后端代码(3个对象类和一堆辅助函数)应采取什么结构,以便从GUI角度更易于维护?

我不是专业的软件工程师,我只知道足够让自己处于危险之中,因此我相信这些问题对于有经验的GUI开发人员(使用任何语言)来说都是相当基本的。我几乎觉得MATLAB中缺乏GUI设计标准(是否存在?)会严重影响我的完成这个项目的能力。这是一个比我曾经承担过的任何项目都要大得多的MATLAB项目,我以前从未考虑过具有多个图形窗口等复杂UI。


一些相关问题:https://dev59.com/T3RC5IYBdhLWcg3wUvUS,https://dev59.com/FE_Ta4cB1Zd3GeqPD8xw。还有:https://dev59.com/4Gsz5IYBdhLWcg3wuKW6,https://dev59.com/4kjSa4cB1Zd3GeqPE2PD,https://dev59.com/2nHYa4cB1Zd3GeqPJ0sg,https://dev59.com/LFzUa4cB1Zd3GeqPzhEX。 - Amro
@Amro,这是一个非常有用的问题,以及您的答案,您可以在答案中包含此链接建议,而不是在评论中。这是进一步阅读的好开始,而且在评论中有点被忽略了 ;) - Robert Seifert
5个回答

28
正如@SamRoberts所解释的,模型-视图-控制器(MVC)模式非常适合设计GUI的架构。我同意目前并没有很多MATLAB示例来展示这样的设计...
下面是一个我写的完整而简单的示例,用于演示基于MVC的MATLAB GUI。
  • 模型表示某个信号的1D函数y(t) = sin(..t..)。它是一个句柄类对象,这样我们可以在不创建不必要的副本的情况下传递数据。它公开可观察属性,允许其他组件监听更改通知。

  • 视图将模型呈现为线性图形对象。视图还包含一个滑块来控制信号属性之一,并侦听模型更改通知。我还包括了一个交互属性,它是特定于视图(而不是模型)的,可以使用右键上下文菜单控制线条颜色。

  • 控制器负责初始化所有内容并响应来自视图的事件,并正确地更新模型。

请注意,视图和控制器是编写为常规函数的,但是如果您更喜欢完全面向对象的代码,则可以编写类。

与通常设计GUI的方式相比,这需要一些额外的工作,但这种架构的优点之一是将数据与表示层分离。特别是在处理复杂的GUI时,这使得代码更加清晰易读,其中代码维护变得更加困难。

这种设计非常灵活,因为它允许您构建相同数据的多个视图。更重要的是,您可以拥有多个同时的视图,只需在控制器中实例化更多的视图实例,然后看看一个视图中的更改如何传播到其他视图!如果您的模型可以以不同的方式进行可视化呈现,则这尤其有趣。

此外,如果您愿意,您可以使用GUIDE编辑器构建界面,而不是通过编程方式添加控件。在这样的设计中,我们将仅使用GUIDE使用拖放来构建GUI组件,但不会编写任何回调函数。因此,我们只对生成的.fig文件感兴趣,并忽略附带的.m文件。我们将在视图函数/类中设置回调。这基本上就是我在View_FrequencyDomain视图组件中所做的,它加载使用GUIDE构建的现有FIG文件。

GUIDE generated FIG-file


Model.m

classdef Model < handle
    %MODEL  represents a signal composed of two components + white noise
    % with sampling frequency FS defined over t=[0,1] as:
    %   y(t) = a * sin(2pi * f*t) + sin(2pi * 2*f*t) + white_noise

    % observable properties, listeners are notified on change
    properties (SetObservable = true)
        f       % frequency components in Hz
        a       % amplitude
    end

    % read-only properties
    properties (SetAccess = private)
        fs      % sampling frequency (Hz)
        t       % time vector (seconds)
        noise   % noise component
    end

    % computable dependent property
    properties (Dependent = true, SetAccess = private)
        data    % signal values
    end

    methods
        function obj = Model(fs, f, a)
            % constructor
            if nargin < 3, a = 1.2; end
            if nargin < 2, f = 5; end
            if nargin < 1, fs = 100; end
            obj.fs = fs;
            obj.f = f;
            obj.a = a;

            % 1 time unit with 'fs' samples
            obj.t = 0 : 1/obj.fs : 1-(1/obj.fs);
            obj.noise = 0.2 * obj.a * rand(size(obj.t));
        end

        function y = get.data(obj)
            % signal data
            y = obj.a * sin(2*pi * obj.f*obj.t) + ...
                sin(2*pi * 2*obj.f*obj.t) + obj.noise;
        end
    end

    % business logic
    methods
        function [mx,freq] = computePowerSpectrum(obj)
            num = numel(obj.t);
            nfft = 2^(nextpow2(num));

            % frequencies vector (symmetric one-sided)
            numUniquePts = ceil((nfft+1)/2);
            freq = (0:numUniquePts-1)*obj.fs/nfft;

            % compute FFT
            fftx = fft(obj.data, nfft);

            % calculate magnitude
            mx = abs(fftx(1:numUniquePts)).^2 / num;
            if rem(nfft, 2)
                mx(2:end) = mx(2:end)*2;
            else
                mx(2:end -1) = mx(2:end -1)*2;
            end
        end
    end
end

View_TimeDomain.m

function handles = View_TimeDomain(m)
    %VIEW  a GUI representation of the signal model

    % build the GUI
    handles = initGUI();
    onChangedF(handles, m);    % populate with initial values

    % observe on model changes and update view accordingly
    % (tie listener to model object lifecycle)
    addlistener(m, 'f', 'PostSet', ...
        @(o,e) onChangedF(handles,e.AffectedObject));
end

function handles = initGUI()
    % initialize GUI controls
    hFig = figure('Menubar','none');
    hAx = axes('Parent',hFig, 'XLim',[0 1], 'YLim',[-2.5 2.5]);
    hSlid = uicontrol('Parent',hFig, 'Style','slider', ...
        'Min',1, 'Max',10, 'Value',5, 'Position',[20 20 200 20]);
    hLine = line('XData',NaN, 'YData',NaN, 'Parent',hAx, ...
        'Color','r', 'LineWidth',2);

    % define a color property specific to the view
    hMenu = uicontextmenu;
    hMenuItem = zeros(3,1);
    hMenuItem(1) = uimenu(hMenu, 'Label','r', 'Checked','on');
    hMenuItem(2) = uimenu(hMenu, 'Label','g');
    hMenuItem(3) = uimenu(hMenu, 'Label','b');
    set(hLine, 'uicontextmenu',hMenu);

    % customize
    xlabel(hAx, 'Time (sec)')
    ylabel(hAx, 'Amplitude')
    title(hAx, 'Signal in time-domain')

    % return a structure of GUI handles
    handles = struct('fig',hFig, 'ax',hAx, 'line',hLine, ...
        'slider',hSlid, 'menu',hMenuItem);
end

function onChangedF(handles,model)
    % respond to model changes by updating view
    if ~ishghandle(handles.fig), return, end
    set(handles.line, 'XData',model.t, 'YData',model.data)
    set(handles.slider, 'Value',model.f);
end

View_FrequencyDomain.m

function handles = View_FrequencyDomain(m)    
    handles = initGUI();
    onChangedF(handles, m);

    hl = event.proplistener(m, findprop(m,'f'), 'PostSet', ...
        @(o,e) onChangedF(handles,e.AffectedObject));
    setappdata(handles.fig, 'proplistener',hl);
end

function handles = initGUI()
    % load FIG file (its really a MAT-file)
    hFig = hgload('ViewGUIDE.fig');
    %S = load('ViewGUIDE.fig', '-mat');

    % extract handles to GUI components
    hAx = findobj(hFig, 'tag','axes1');
    hSlid = findobj(hFig, 'tag','slider1');
    hTxt = findobj(hFig, 'tag','fLabel');
    hMenu = findobj(hFig, 'tag','cmenu1');
    hMenuItem = findobj(hFig, 'type','uimenu');

    % initialize line and hook up context menu
    hLine = line('XData',NaN, 'YData',NaN, 'Parent',hAx, ...
        'Color','r', 'LineWidth',2);
    set(hLine, 'uicontextmenu',hMenu);

    % customize
    xlabel(hAx, 'Frequency (Hz)')
    ylabel(hAx, 'Power')
    title(hAx, 'Power spectrum in frequency-domain')

    % return a structure of GUI handles
    handles = struct('fig',hFig, 'ax',hAx, 'line',hLine, ...
        'slider',hSlid, 'menu',hMenuItem, 'txt',hTxt);
end

function onChangedF(handles,model)
    [mx,freq] = model.computePowerSpectrum();
    set(handles.line, 'XData',freq, 'YData',mx)
    set(handles.slider, 'Value',model.f)
    set(handles.txt, 'String',sprintf('%.1f Hz',model.f))
end

Controller.m

function [m,v1,v2] = Controller
    %CONTROLLER  main program

    % controller knows about model and view
    m = Model(100);           % model is independent
    v1 = View_TimeDomain(m);  % view has a reference of model

    % we can have multiple simultaneous views of the same data
    v2 = View_FrequencyDomain(m);

    % hook up and respond to views events
    set(v1.slider, 'Callback',{@onSlide,m})
    set(v2.slider, 'Callback',{@onSlide,m})
    set(v1.menu, 'Callback',{@onChangeColor,v1})
    set(v2.menu, 'Callback',{@onChangeColor,v2})

    % simulate some change
    pause(3)
    m.f = 10;
end

function onSlide(o,~,model)
    % update model (which in turn trigger event that updates view)
    model.f = get(o,'Value');
end

function onChangeColor(o,~,handles)
    % update view
    clr = get(o,'Label');
    set(handles.line, 'Color',clr)
    set(handles.menu, 'Checked','off')
    set(o, 'Checked','on')
end

MVC GUI1 MVC GUI2

在上面的控制器中,我实例化了两个独立但同步的视图,它们都表示并响应同一基础模型中的变化。一个视图显示信号的时域,另一个使用FFT显示频域表示。


感谢通过 hgload 用 GUIDE-generate .fig 连接点,即使您决定不使用它生成的回调,GUIDE 仍然是一个有用的工具。 - chappjc
为了扩展这个应用,我可能需要为每个GUI构建一个不同的模型/控制器/视图?也许我可以保持单一模型并拥有多个控制器/视图。但是当应用程序有很多窗口时,这种方法如何扩展? - Dang Khoa
@Amro,虽然你提供了如此详尽的示例(所以+1),但我认为你的答案在某些方面并没有展示最佳实践或建议。特别是,我建议i)不要使用GUIDE - 有很多原因,但特别是因为.fig文件不能进行版本控制(至少不能进行差异化比较),因此以这种方式开发的应用程序将更难维护ii)... - Sam Roberts
@SamRoberts:谢谢。i)你说得对,我自己也不建议以这种方式使用GUIDE。我展示了两个视图实现,其中一个使用GUIDE只是为了证明它是可能的。ii)我在这里也同意,但请记住,视图不一定要创建顶级图形;我们可以设计一个视图,使其在uipanel容器内绘制,这样多个视图就可以组合成一个图形。GUI布局工具箱是一个很好的包,在MVC模式中也可以使用。 - Amro
有很多关于MVC最佳实践的在线资料,尽管我不确定其中有多少适用于MATLAB世界!(搜索:关注点分离、n层架构、MVC持久化模式、控制反转等)。您还可以开始考虑对MVC的各个组件进行单元测试、模拟测试等。最终,我认为您可以选择在应用程序中集成多少内容,毕竟这是很多工作 :) - Amro
显示剩余6条评论

10

UserData属性是MATLAB对象的一个有用但过时的属性。"AppData"方法套件(即setappdatagetappdatarmappdataisappdata等)提供了一种更好的替代方法,相比之下,使用get/set(hFig,'UserData',dataStruct)方法更加笨拙。实际上,为了管理GUI数据,GUIDE采用了guidata函数,它只是setappdata/getappdata函数的包装器

我想到的几个AppData方法优于'UserData'属性的优点:

  • 为多个异构属性提供更自然的接口。

    UserData 仅限于单个变量,需要你设计另一层数据组织(如结构体)。比如你想存储一个字符串 str = 'foo' 和一个数字数组 v=[1 2]。使用 UserData,你需要采用结构体方案,例如 s = struct('str','foo','v',[1 2]); 并且每当你需要其中任何一个属性时,都要 set/get 整个结构体(例如 s.str = 'bar'; set(h,'UserData',s);)。使用 setappdata 更直接(也更高效): setappdata(h,'str','bar');

  • 对底层存储空间提供保护性接口。

    虽然 'UserData' 只是普通的图形句柄属性,但包含应用程序数据的属性不可见,尽管可以通过名称访问它('ApplicationData',但不要这样做!)。必须使用 setappdata 来更改任何现有的 AppData 属性,这可以防止在更新单个字段时意外破坏整个 'UserData' 的内容。在设置或获取 AppData 属性之前,可以使用 isappdata 验证命名属性的存在,这有助于异常处理(例如在设置输入值之前运行进程回调)和管理 GUI 或其控制的任务的状态(例如通过存在某些属性来推断进程状态并适当更新 GUI)。

重要的区别在于“UserData”和“ApplicationData”属性,“UserData”默认为[](空数组),而“ApplicationData”本质上是一个结构体。这个区别,再加上“setappdata”和“getappdata”没有M文件实现(它们是内置的),表明使用“setappdata”设置命名属性不需要重写整个数据结构的内容。 (想象一下执行结构体字段的原地修改的MEX函数 - MATLAB能够通过将结构体作为“ApplicationData”句柄图形属性的基础数据表示来实现该操作。)
guidata 函数是 AppData 函数的包装器,但它仅限于单个变量,例如 'UserData'。这意味着您必须覆盖包含所有数据字段的整个数据结构才能更新单个字段。一个明显的优点是,您可以在不需要实际图形句柄的情况下从回调中访问数据,但就我而言,如果您熟悉以下语句,则这并不是一个很大的优势:
hFig = ancestor(hObj,'Figure')

此外,正如MathWorks所述, 存在效率问题:
保存大量数据在“handles”结构中有时可能会导致显着的减速,特别是如果在GUI的各个子函数中经常调用GUIDATA。因此,建议仅使用“handles”结构来存储对图形对象的句柄。对于其他类型的数据,应使用SETAPPDATA和GETAPPDATA将其存储为应用程序数据。
这个声明支持我的说法,即当使用setappdata修改单个命名属性时,整个“ApplicationData”不会被重写。(另一方面,guidata将“handles”结构压缩到名为“UsedByGUIData_m”的“ApplicationData”字段中,因此很明显当更改一个属性时为什么guidata需要重新编写所有GUI数据)。
嵌套函数需要非常少的努力(无需辅助结构或函数),但它们显然限制了数据的范围,使得其他GUI或函数无法访问该数据,除非将值返回到基础工作区或公共调用函数。这显然阻止了您将子函数拆分到单独的文件中,但只要传递图形句柄,您就可以轻松使用“UserData”或AppData来实现此操作。
总之,如果您选择使用句柄属性来存储和传递数据,则可以同时使用guidata管理图形句柄(不是大量数据)setappdata/getappdata用于实际程序数据。它们不会互相覆盖,因为guidataApplicationData中为handles结构创建了一个特殊的'UsedByGUIData_m'字段(除非您犯了使用该属性的错误!)。再次强调,不要直接访问ApplicationData
然而,如果您熟悉面向对象编程,使用类实现GUI功能可能更加清晰。可以将处理程序和其他数据存储在成员变量中,而不是句柄属性,并在方法中使用回调,这些方法可以存在于类或包文件夹下的单独文件中。MATLAB Central File Exchange上有一个很好的示例。该示例演示了如何通过类简化传递数据,因为不再需要不断获取和更新guidata(成员变量始终是最新的)。但是,还需要管理退出时的清理任务,该提交通过设置图形的closerequestfcn来完成,然后调用类的delete函数。该提交与GUIDE示例非常相似。
这些是我看到的亮点,但MathWorks讨论了更多细节和不同的想法。请参见此处以及UserData vs. guidata vs. setappdata/getappdata官方回答

最终,getappdata/setappdata 只是获取/设置存储在隐藏图形属性“ApplicationData”中的结构体中,而不是在“UserData”中的包装器。因此,从性能上讲,它们应该非常相似。 - sebastian
1
@sebastian 我不一定会得出那样的结论。首先,MathWorks声明getappdata/ setappdataguidata具有更好的性能,这意味着 ApplicationData 中的struct不需要全部重写。 此外,如果您执行get(h,'ApplicationData'),则可以看到它本质上是一个结构体,而get(h,'UserData')[],需要您在该属性中存储一个结构体。 getappdata/setappdata没有M文件实现的事实表明了一种更有效的访问ApplicationData struct属性的方法。 - chappjc
那么您也会将GUI数据(例如“句柄结构”)存储为ApplicationData吗?我想这并没有什么区别。只是想定义一个约定并坚持它。 - Dang Khoa
1
@DangKhoa 您可以使用 guidata 存储图形句柄(而不是用户数据),并使用 set/getappdata 存储实际数据。它们不会互相覆盖,因为 guidata 使用 ApplicationData 的特殊 'UsedByGUIData_m' 属性(除非您犯了使用该属性的错误!)。来自这里:"使用 getappdata、setappdata 和 rmappdata 函数不会影响 GUI 数据。" 重申一遍,不要直接访问 'ApplicationData' - chappjc

7
我不同意 MATLAB 不适合实现(即使是复杂的)GUI 的说法 - 它完全可以胜任。
然而,以下是真实情况:
1. MATLAB 文档中没有关于如何实现或组织复杂 GUI 应用程序的示例。 2. 所有简单 GUI 的文档示例使用的模式都无法很好地扩展到复杂 GUI 上。 3. 特别是 GUIDE(自动生成 GUI 代码的内置工具)生成的代码非常糟糕,如果您要自己实现某些内容,则是一个可怕的样例。
由于这些原因,大多数人只接触到非常简单或非常糟糕的 MATLAB GUI,他们最终认为 MATLAB 不适合制作 GUI。
根据我的经验,在 MATLAB 中实现复杂的 GUI 的最佳方法与其他语言相同 - 遵循一个常用的模式,例如 MVC(Model-View-Controller)。
但是,这是一种面向对象的模式,因此您首先必须熟悉 MATLAB 中的面向对象编程,特别是事件的使用。使用面向对象的方式组织应用程序应该意味着不需要使用所有恶劣的技术(例如 setappdata、guidata、UserData、嵌套函数作用域和来回传递多个数据副本),因为所有相关的事情都可用作类属性。
我知道的 MathWorks 发布的最好的示例在 MATLAB Digest 的这篇文章中,即使那个示例非常简单,但是它可以让您了解如何入门,如果您深入研究 MVC 模式,应该就会明白如何扩展它。
此外,我通常会大量使用包文件夹来组织 MATLAB 中的大型代码库,以确保没有命名冲突。
最后一个提示 - 使用GUI Layout Toolbox,位于 MATLAB Central。它使得许多 GUI 开发方面变得更加容易,特别是实现自动调整大小行为,并提供了几个其他 UI 元素可供使用。
希望这有所帮助!
编辑:在 MATLAB R2016a 中,MathWorks 推出了 AppDesigner,这是一个新的 GUI 构建框架,旨在逐步取代 GUIDE。
AppDesigner 在几个方面与以前的 MATLAB GUI 构建方法存在重大差异(最深入的是,生成的基础图形窗口基于 HTML 画布和 JavaScript,而不是 Java)。它是由 R2014b 中引入的 Handle Graphics 2 所启动的一条道路上的另一步,并且无疑会在未来的版本中进一步发展。
但是,AppDesigner 对这个问题产生的一个影响是,它生成比 GUIDE 更好得多的代码 - 它非常清晰、面向对象,并适合作为 MVC 模式的基础。

这非常有帮助!我熟悉MATLAB的面向对象编程和事件,也在使用GUI布局工具箱(主要是TabPanel)。我会深入了解MVC设计模式。 - Dang Khoa
MVC是一个众所周知的设计模式,用于将数据层与表现层分离。我发布了一个示例,其中包含一种可能的MVC实现。 - Amro

2
我对GUIDE生成函数的方式感到非常不舒服。(考虑一下你想从另一个GUI调用一个GUI的情况)
我强烈建议您使用句柄类编写面向对象的代码。这样,您可以做一些花哨的事情(例如这个),而不会迷失方向。对于组织代码,您可以使用+@目录。

1
我认为GUI代码的结构与非GUI代码并没有根本上的不同。
将属于同一类别的内容放在一起,可以放在utilhelpers目录下,也可以根据内容创建一个包。
个人而言,我不喜欢一些MATLAB人员所持有的“一个函数一个m文件”的哲学。将一个函数放在一个m文件中,例如:
function pushbutton17_callback(hObject,evt, handles)
    some_text = someOtherFunction();
    set(handles.text45, 'String', some_text);
end

将其放入单独的文件中根本没有意义,除非存在一种完全不会从自己的GUI以外的地方调用它的情况。

然而,您可以通过将父容器简单传递来创建某些组件,以模块化的方式构建GUI本身:

 handles.panel17 = uipanel(...);
 createTable(handles.panel17); % creates a table in the specified panel

这也简化了某些子组件的测试 - 您可以在空图上简单地调用createTable,并测试表格的某些功能,而无需加载完整的应用程序。

当我的应用程序变得越来越庞大时,我开始使用两个额外的项目:

使用监听器而不是回调函数,它们可以显著简化GUI编程。

如果您有非常大的数据(例如来自数据库等),那么实现一个包含这些数据的句柄类可能是值得的。 将该句柄存储在guidata/appdata中的某处会显著提高get/setappdata的性能。

编辑:

监听器而非回调函数:

pushbutton是不好的例子。按下按钮通常仅在特定操作上触发,因此在我看来回调函数很好。 例如,在我的情况下,一个主要的优点是,通过编程更改文本/弹出列表不会触发回调,而监听器在其StringValue属性上被触发。

另一个例子:

如果有一些中心属性(例如像某些输入数据源)多个组件依赖于它,那么使用监听器非常方便,可以确保所有组件在属性更改时得到通知。 每个对此属性“感兴趣”的新组件都可以添加自己的监听器,因此无需集中修改回调函数。 这允许GUI组件具有更模块化的设计,并使添加/删除此类组件更容易。


你能举一个使用监听器而不是回调函数的例子吗?如果要处理按下 pushbutton 对象这种事件,如何避免使用回调函数? - Dang Khoa
我在我的回答中添加了一个例子。 - sebastian

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接