当前位置: 首页 > 工具软件 > wxErlang > 使用案例 >

wxErlang 的案例学习(2)

童浩言
2023-12-01

二、模块sudoku_gui.erl
源代码太长,不全贴了。
学案例读代码的过程中,会遇到各种问题,由此促使有针对性的读文档资料,寻找答案。
这时去读枯燥乏味的文档,感觉会很不一样,仿佛好看好懂了。
(1)wxErlang的编程模式
把我们带进这个模块的,是sudoku.erl中的函数调用 ?TC(sudoku_gui:new(self())),
一切都从本模块的函数new/1开始。
new(Game) ->
    wx:new(),
    wx_object:start_link(?MODULE, [Game], []).
从这个函数中,看到的是运行wxErlang的内建函数,看不到调用本模块创建GUI界面函数的语句,有点疑惑不解。
需要看看文档了。先来了解wx:new(),文档是erl5.7.1/lib/wx-0.98.1/doc/html/wx.html
我觉得,其中最重要的一句话是:wx, A port of wxWidgets. 
在Erlang平台上,wxErlang是wxWidgets的化身,在逻辑上是“外部程序”,即提供GUI功能的服务器。
wx是Erlang与wxWidgets联络的接口(port)。wx:new()的结果,是启动了GUI服务器。
这个服务器在内存中装配成什么样子呢?模块wx_object对此有着原则要求。
wxErlang的模块,与wxWidgets的类(class)基本上是一一对应的。wx,代表wxWidgets根本的基类。
类,是个逻辑概念,是给人用的。要让机器使用,必须实例化,做成object。wx_object不是wxWidgets的类,
而是wx在内存里的具体物理实现。可以把wx_object看做gen_server的behaviour。
说到这里,可见wxErlang已进OTP的套路。
OTP虽然不是OOP,但确有OOP的影子。它的主要招法callback函数,与OOP的override“重设”似曾相识。
wx_object是OTP的行为(behaviour),行为由动作组成,动作是反应性的,所以叫callback。由此可见,把callback译成“反应”是对的,译成“回调”不妥。
当然,现在是用Erlang,还是要用它的说法。它说的是:The user module should export,用户程序模块应该导出以下函数:
init(Args) 
handle_call(Msg, {From, Tag}, State)
handle_event(#wx{}, State)
handle_info(Info, State)
本模块定义实现这些函数,就完成了wx服务器的结构定义。
Erlang是说明性语言,不是过程性语言。在函数new(Game) 中的2个语句,宣布wx服务器建立,具体的操作过程在“黑盒”中,不必关心。
不仅如此,创建object返回的“引用”(reference 这也是个笨拙的直译),是由机器直接操作使用的,并且会自动变化,无需人的干预。
也就是说,在Erlang程序中,“引用”是当做“值”使用的,没有OOP编程中的用法,如ref.title='xxx'; ref->open()之类。
语句wx_object:start_link(?MODULE, [Game], []),返回的“引用”是wxWindow() ,系统内部会自动处理它,你无需操心。
值得你关注的是这句话:
start_link(Name, Mod, Args, Options) -> wxWindow() 
    ……
    Starts a generic wx_object server and invokes Mod:init(Args) in the new process. 
意思是说,函数start_link在新进程中,启动wx对象的总服务器,并且调用本模块的函数init(Args)。
这是问题的关键。总算可以看见些“活动”的东西,看见sudoku GUI界面的装配过程。下面的事情变得简单了。
(2)本模块函数init/1
init([Game]) ->
    {Frame, Board} = wx:batch(fun() -> create_window() end),
    Game ! {gfx, self()},
    {Frame, init_printer(#gs{board=Board,game=Game,frame=Frame})}.
这里的Game,就是sudoku.erl主进程的Pid。
wx:batch/1 的作用,是为了保障函数create_window()调用时,其中的wx装配界面的命令集中优先执行,不受其他事件干扰。
Game ! {gfx, self()}, 向sudoku.erl主进程发送消息,self()是本子进程的Pid。
{Frame, init_printer(#gs{board=Board,game=Game,frame=Frame})}.
相当于:
State = init_printer(#gs{board=Board,game=Game,frame=Frame}),
{Frame, State}.
(3)本模块函数create_window/0
本模块有以下几点值得关注。
①产生并返回当做“值”使用的“引用”
……
Frame = wxFrame:new(wx:null(), -1, "Sudoku", []),
……
Board = sudoku_board:new(Panel),
……
{Frame, Board}.
②函数中用的宏,定义在sudoku.hrl,如:?NEW, ?OPEN, ?SAVE
③GUI界面对象元件的用法,直接阅读文档,难度不大。
(4)本模块的行为反应函数
由于本模块的wx_object是行为,与OTP的gen_server 的行为动作相似,包括:
● init/1 - 服务器的初始化;
● handle_call/3 - 处理对服务器的同步调用。调用服务器的客户端被阻塞,直到本函数返回。
● handle_info/2 - 是起着“收容”作用的函数。服务器收到的信息,如果不是同步调用或异步调用,就交由这个函数处理。例如,如果你的服务器与其他进程相连接,那么,要求退出进程的信息,就是这个函数处理。
● terminate/2 - 关闭服务器时,调用这个函数,做些善后处理。
● code_change/3 - 服务器运行中更新时,调用这个函数。
值得关注的是wx_object行为特有的GUI反应函数:handle_event/2
在GUI编程中,事件的处理是相对较难的地方,因此多说几句。处理事件的子句很多,下面只举一例。
handle_event(#wx{id=?HINT, event=#wxCommand{type=command_button_clicked}},
    S = #gs{game=G}) ->
    G ! {solve,false},
    {noreply,S};
函数参数是2个记录,#wx和#gs。#wx有2个数据项id和event。
id是发出事件的对象(元件),在这里是?HINT,即 函数create_window()中创建的那个按钮:
    Hint  = wxButton:new(Panel, ?HINT, [{label, "Hint"}]),
event也是记录#wxCommand,类型是command_button_clicked,即按下按钮的命令。
记录#gs的数据项game=G,是sudoku.erl主进程的Pid。
G ! {solve,false}, 向主进程发送消息;
{noreply,S};返回事件反应的结果。
(5)本模块的事件响应函数 dialog/2
本模块内有一大堆dialog/2子句,语句虽多难度不大,不展开说了。只提一下调用它的handle_event/2
handle_event(#wx{id=ID, event=#wxCommand{}}, S) when ID > 125 ->
    New = dialog(ID, S),
    {noreply, New};
在条件 when ID > 125 符合后,去调用dialog/2。这个条件,可到sudoku.hrl中查看:
-define(OPEN,  130).
-define(SAVE,  131).
-define(RULES, 132).
-define(CLEAR, 135).
-define(SHOW_ERROR, 136).
-define(PRINT, 137).
-define(PRINT_PRE, 138).
-define(PRINT_PAGE_SETUP, 139).
-define(TRIVIAL, 240).
-define(EASY,    235).
-define(NORMAL,  230).
-define(HARD,    225).
-define(HARDEST, 210).
要明确是哪个对象(元件)发出的事件,可在本模块sudoku_gui.erl中查找相关handle_event对应的宏,如?OPEN。

 

 类似资料: