Erlang 圖形編程 - wxErlang GUI

wxErlang GUI

wxErlang GUI

Erlang 這門編程語言通常用于服務(wù)器方面,雖然它也有類似 Wings 3D 這樣圖像密集的應(yīng)用。wxWidget 是對 Erlang 支持最好的圖像 API,它為 GUI 編程提供一個大型,成熟,穩(wěn)定的跨平臺 API。

在這個部分,假設(shè)你早已:

  • 安裝了 Erlang
  • 已經(jīng)知道怎么使用 Erlang shell
  • 有用過合適的文本編輯器寫程序

并非所有wx調(diào)用都產(chǎn)生一個直觀的圖形顯示;在 Erlang shell 中,通常你只能看到返回值。這些值可能是神秘的,尤其是如果你過去沒有用過 Erlang。因為 Erlang 是一門函數(shù)式編程語言,所以每個wx調(diào)用都會返回一個值。這些值大多是 tuple。而這些 tuple 又大多有記錄 record 的內(nèi)容。在記錄格式下理解 wx 返回值會更容易。Erlang shell 需要被告知 wx 的定義。

尋找在你系統(tǒng)上 wx 定義在哪,可以輸入這個:

1> My_wx_dir = code:lib_dir(wx).
"c:/Program Files/erl10.4/lib/wx-1.8.8"

從剛剛獲取的那個目錄讀取 wx 定義的 record 類型:

rr (My_wx_dir ++ "/include/wx.hrl"). 
rr (My_wx_dir ++ "/src/wxe.hrl").

兩個 rr 調(diào)用都應(yīng)該返回一系列模塊,如果 rr 調(diào)用出錯,你將得到空列表。 打開你的文本編輯器。把上面三行代碼復(fù)制粘貼到一個臨時文件。然后,當你開始一個新小節(jié),或者重新開始小節(jié),或者因為崩潰不得不重啟,你可以把它們復(fù)制粘貼到 shell 里?,F(xiàn)在就試試,確保它們能正常工作。退出 Erlang,重啟,然后復(fù)制粘貼這些代碼到 shell。

很不幸,我們不能在 shell 中使用 Erlang 宏定義。不過這也是另一個需要定位 wx.hrl 文件的理由:為了在 shell 中使用,我們得查詢需要的 wx 宏符號所對應(yīng)的值。

Wx=wx:new().
#wx_ref{ref = 0,type = wx,state = []

它運行了嗎?可能有消息說對稱多處理 SMP 沒有開啟,或者“SMP emulator required”。在一些 Windows Erlang 發(fā)行版中 SMP 沒有默認開啟。退出 Erlang shell 然后帶參 -smp 重啟,在DOS命令行中就像這樣: werl.exe -smp 如果 wx:new() 正常運行,將會返回一個記錄。

在 wxWidgets 中,一個窗口相當于一個 frame。讓我們寫一個簡單的程序,然后添加它。輸入下面的代碼可以生成一個 frame:

F=wxFrame:new(Wx, -1, "Hello, World!").

但是屏幕上沒有任何改變。為什么?我們必須提出想看看frame的請求它才會出現(xiàn)。輸入:

wxFrame:show(F).

它會返回 true,并且你就會看到一個窗體。

從 shell 異常中恢復(fù)

只需要點擊關(guān)閉按鈕就能關(guān)閉 frame。但是別那么做,先試試下面這個無意義的調(diào)用;

nothing:doint().

這會讓 frame 消失,隨之出現(xiàn)的還有異常錯誤消息。這是因為 wxWidgets 在它 shell 的進程運行圖形程序,如果 shell 中出現(xiàn)異常又不捕獲,它就會當即被殺死。

僅僅鍵盤輸入錯誤就有可能導(dǎo)致 GUI 完全丟失,這種情況而且會經(jīng)常發(fā)生。無論在哪只要你引發(fā)了錯誤就要重新輸入一遍。沒人愿意來上幾次吧?所以,在開始教程之前,輸入下面的代碼:

catch_exception (true).

現(xiàn)在休息一下吧!這樣設(shè)置后可以讓GUI程序無視引發(fā)錯誤的地方繼續(xù)工作。把上面的代碼全都放到一個臨時文件,像這樣:

catch_exception (true). 
My_wx_dir = code:lib_dir(wx). 
rr (My_wx_dir ++ "/include/wx.hrl"). 
rr (My_wx_dir ++ "/src/wxe.hrl"). 
Wx=wx:new(). 
F=wxFrame:new(Wx, -1, "Hello, World!"). 
wxFrame:show(F).

當 frame 在屏幕上現(xiàn)實時,輸入:

wxFrame:destory(F).

它應(yīng)該返回 ok 然后 frame 銷毀消失了。

StatusBar 狀態(tài)欄

就當是開心一下,創(chuàng)建多個frame:

  • 創(chuàng)建一個標題為"Hey!"的wxFrame,變量名為 F1
  • 顯示 F1
  • 創(chuàng)建一個標題為"Boo!"的wxFrame,變量名為 F2
  • 顯示 F2.
  • 使用 wxFrame:destroy 將兩個 frame 銷毀。

別把這些代碼放到臨時文件,我們的 lesson 要從第一個 destroy 調(diào)用繼續(xù)。

catch_exception (true). 
My_wx_dir = code:lib_dir(wx). 
rr (My_wx_dir ++ "/include/wx.hrl"). 
rr (My_wx_dir ++ "/src/wxe.hrl"). 
Wx=wx:new(). 
F1=wxFrame:new(Wx, -1, "F1"). 
wxFrame:show(F1).
F2=wxFrame:new(Wx, -1, "F2"). 
wxFrame:show(F2).

狀態(tài)欄不僅方便程序功能,也便于調(diào)試。

wxFrame:createStatusBar(F).

現(xiàn)在你的 frame 就會增加一個狀態(tài)欄。

將一些文字放到狀態(tài)欄中:

wxFrame:setStatusText(F, "Quiet here.").

花一點時間把上面這些代碼復(fù)制粘貼到你的臨時文件,嘗試向狀態(tài)欄設(shè)置其他文字,然后恢復(fù)為“Quiet here”:

SB = wxFrame:getStatusBar(F). 
wxStatusBar:pushStatusText(SB, "A LITTLE LOUDER NOW."). 
wxStatusBar:popStatusText(SB).

現(xiàn)在應(yīng)該已經(jīng)回到了之前你向狀態(tài)欄添加文字的樣子。

Menu 菜單欄

按照慣例 wxWidgets 中的 frame 都會有一個菜單欄。這樣看起來狀態(tài)欄菜單欄沒什么區(qū)別。然而,菜單欄通常由其他東西組成:它們需要被組合到一起。

在 wxWidgets 中,復(fù)雜的東西通常都是由簡單的東西開始一步步構(gòu)建的。wxWidgets 的 API 不假設(shè)新建的復(fù)雜的東西包含任何簡單的東西。對于越復(fù)雜的東西,所需的構(gòu)建步驟就越多。

讓我們盡快生成一個可見的菜單欄。當你完成后記得復(fù)制下面的代碼到你的臨時文件,

生成一個菜單欄,輸入:

MenuBar = wxMenuBar:new().
#wx_ref{ref = 37,type = wxMenuBar,state = []} 

但是 frame 仍然沒有菜單欄吧?我們有看到菜單欄關(guān)聯(lián) frame 嗎?是的,F(xiàn) 是到目前為止你僅有的 frame,但 wx 不假設(shè)你想把 MenuBar 放到 F 里面去。

嘗試一下將 MenuBar 設(shè)置為 F 的一部分:

wxFrame:setMenuBar (F, MenuBar).
wxFrame:getMenuBar (F).

它可能會返回ok,但是...窗口還是沒有任何東西!的確發(fā)生了菜單欄和 frame 的關(guān)聯(lián)。問題是:frame 顯示這個已經(jīng)關(guān)聯(lián)的菜單欄并沒有菜單項,我們得做點什么。

下面幾步將添加菜單項到菜單欄,然后顯示它。

大多數(shù) GUI 應(yīng)用程序都有一個 File(文件)菜單。輸入這個:

FileMn = wxMenu:new().
#wx_ref{ref = 37,type = wxMenu,state = []}

又是這樣,wxWidgets 不知道你想把這個菜單添加到哪,所以不會顯示。你必須告訴 FileMn 它應(yīng)該被放到哪個菜單欄?,F(xiàn)在我們把 FileMn 放到 F 的菜單欄 MenuBar 里:

wxMenuBar:append (MenuBar, FileMn, "&File").

“&File” 前面的 “&” 符號表示你可以輸入快捷鍵 Alt-F 使用它。

現(xiàn)在有菜單了,但是一個好的菜單沒有菜單項怎么行。點擊 File 菜單或者 Alt-F,好吧,沒有任何東西,那是怎么回事?

需要添加一個菜單項,每個 File 菜單都應(yīng)該有一個 Quit 菜單項,讓我們也添加一個,并添加到文件菜單上,輸入:

Quit = wxMenuItem:new ([{id,400},{text, "&Quit"}]).
wxMenu:append (FileMn, Quit).

現(xiàn)在,點擊 File 菜單,可以看到 Quit 菜單項了。

回顧上述設(shè)置的菜單的所有代碼,你會看到:

MenuBar = wxMenuBar:new(). 
wxFrame:setMenuBar (F, MenuBar). 
FileMn = wxMenu:new(). 
wxMenuBar:append (MenuBar, FileMn,"&File"). 
Quit = wxMenuItem:new ([{id,400},{text, "&Quit"}]). 
wxMenu:append (FileMn, Quit).

我們還可以添加什么呢?每一個得體的應(yīng)用程序都有一個 Help 菜單。然后 Help 菜單通常有一個 About 菜單項。

重復(fù)你之前添加 File 菜單所用的 new append 命令:

HelpMn = wxMenu:new(). 
wxMenuBar:append (MenuBar, HelpMn, "&Help").
About = wxMenuItem:new ([{id,500},{text,"About"}]). 
wxMenu:append (HelpMn, About).

添加 About 菜單后,同樣重復(fù)之前將 Quit 菜單項添加到 File 菜單的步驟,就會得到另外一個 Help 菜單。點一下 Help 菜單,你會看到 About 菜單。

花一點時間把代碼復(fù)制粘貼到你的臨時文件。

Events 事件

到目前為止,我們所做的都沒有涉及事件。你可能認為 Erlang wxWidgets 沒有事件。如果你現(xiàn)在輸入 flush()., 你就不會那樣想了。 事實上,在 wxWidgets 中每個鼠標點擊都會觸發(fā)事件。它們被wx以默認的一些方式處理。通常,wx 的默認處理方式是忽略它們。讓我們捕獲事件,看看它到底是什么樣的。

使用 connect 聯(lián)接并查看事件,輸入:

wxFrame:connect (F, close_window).

點擊 frame 上的關(guān)閉按鈕,然后輸入:

flush().

你會看到這樣的輸出:

Shell got {wx,-202,{wx_ref,35,wxFrame,[]},[],{wxClose,close_window}}

注意,現(xiàn)在點擊關(guān)閉按鈕不會真正的關(guān)閉一個 frame。你將重寫這個默認行為。 多點幾次關(guān)閉按鈕,然后點擊最大化窗口按鈕,最小化窗口按鈕。然后再次輸入 flush().。你會看到 close_window 事件,但是沒有最大化最小化事件。

同樣請注意 shell 怎樣輸出它收到的事件:它不會使用之前讀取的 wx 定義。你只會看到原始 tuple。這使得我們知曉這些 wx 事件是什么的難度增加。

有一個使用記錄定義查看事件的方法。下面的 fun 返回一個 receive 接收到的事件,輸入:

Ev = fun() -> receive E->E after 0 -> empty end end.

點擊關(guān)閉按鈕,然后調(diào)用事件讀取器:

Ev().

你會看到類似這樣的東西:

#wx{id = -202,
    obj = #wx_ref{ref = 35,type = wxFrame,state = []},
    userData = [],
    event = #wxClose{type = close_window}}

讓我們嘗試關(guān)聯(lián) connect 菜單選擇事件,輸入:

wxFrame:connect (F, command_menu_selected).

嘗試選擇菜單。選擇File->Quit,然后選擇File->About。然后輸入Ev().看看生成了哪些事件。除了id外,返回的事件應(yīng)該都是一樣的。

#wx{id = 400,
    obj = #wx_ref{ref = 35,type = wxFrame,state = []},
                  userData = [],
                  event = #wxCommand{type = command_menu_selected,
                     cmdString = [],
                     commandInt = 0,
                     extraLong = 0}}

知道發(fā)生了什么事件很有用,有時它有助于觀察細節(jié)。但是大多數(shù)時候,我們只想當事件發(fā)生時做出我們希望的動作。所以我們必須捕獲事件,然后搞懂怎樣給它添加一個動作。

wx 中的事件由回調(diào)函數(shù)處理。首先,生成一個回調(diào)函數(shù)。輸入:

Ding = fun (_,_) -> wx_misc:bell() end.

試試,給它傳入正確的參數(shù)。

Ding(#wx{},#wx_ref{}).

它會響鈴嗎?會的。

現(xiàn)在將它關(guān)聯(lián)到你的 frame 的 close_windows 事件上:

wxFrame:connect (F, close_window, [{callback, Ding}]).

再試試點擊關(guān)閉按鈕,就會有嗶嗶聲。試試調(diào)用 Ev(). 它不再返回 close_window 事件。

因為簡單的嗶嗶聲對于這個關(guān)閉窗口事件是沒有實際意義的,你可能想解除關(guān)聯(lián)

wxFrame:disconnect (F, close_window).

Dialog 對話框

一個“About”菜單項應(yīng)該給我們顯示一個模態(tài)對話框。但是怎樣生成這個對話框?這里是最簡單的方法。

生成一個模態(tài)對話框,輸入下面這行代碼:

D = wxMessageDialog:new (F, "Let's talk.").
#wx_ref{ref = 43,type = wxMessageDialog,state = []}

它應(yīng)該返回一個類似 #wx_ref 這樣的回應(yīng),但是屏幕上不會有任何顯示。

要想顯示對話框并與之交互,輸入:

wxMessageDialog:showModal (D).

在你的屏幕上就會看到彈出的對話框。

因為對話框是模態(tài)的,所以直到你點 OK 之前 shell 都不會有任何返回值。返回值應(yīng)該是 5100。如果你看看 wx.hrl,你就會知道它代表 wxID_OK。

wxErlang Hello

參考代碼來自 Erlang 源代碼中,wx 模塊提供了 Examples。

這是一個基本窗體演示,可以通過以下命令編譯執(zhí)行:

erlc hello.erl && erl -noshell -s hello start -s init stop

不像專門為特定系統(tǒng)設(shè)計的快速 GUI 原型程序,如 Tcl/Tk,從 Erlang shell 命令行開始圖形界面開發(fā)之前有一些準備工作要做。

注意 Erlang 是面向函數(shù)式的編程,和面向?qū)ο蟮木幊淘诒磉_式上有些差別,如以下 OOP 代碼:

wxWindow MyWin = new wxWindow();
MyWin.CenterOnParent(wxVERTICAL);
...
delete MyWin;

Erlang 對應(yīng)的代碼:

MyWin = wxWindow:new(),
wxWindow:centerOnParent(MyWin, [{dir,?wxVERTICAL}]),
...
wxWindow:destroy(MyWin),

很多對象模塊都提供了 destroy 解構(gòu)函數(shù),這本來在 OOP 中是對象的析構(gòu)函數(shù),在 Erlang 中則就模塊函數(shù)的方式提供。

對于 wxWidgets 那些非類實現(xiàn)的方法,在 Erlang 中使用 wx_misc 模塊實現(xiàn)。
wxWidgets 對象和 Erlang 的對應(yīng)參考:

wxWidgets 對象 Erlang 對象
wxPoint {Xcoord,Ycoord}
wxSize {Width,Height}
wxRect {Xcoord,Ycoord,Width,Height}
wxColour {Red,Green,Blue[,Alpha]}
wxPoint {Xcoord,Ycoord}
wxString unicode:charlist()
wxGBPosition {Row,Column}
wxGBSpan {RowSpan,ColumnSPan}
wxGridCellCoords {Row,Column}

使用 wxErlang 時,需要刻意調(diào)用 wx:new() 來執(zhí)行 GUI 程序的初始化,處理環(huán)境變量和內(nèi)存映射。為了在多線程中共用這些配置,需要調(diào)用 wx:get_env/0wx:set_env/1 來獲取當前的活動環(huán)境變量,或設(shè)置給新進程使用。兩個進程都各自調(diào)用 wx:new() 就不能互相使用對方的對象。

wx:new(), 
MyWin = wxFrame:new(wx:null(), 42, "Example", []),
Env = wx:get_env(),
spawn(fun() -> 
       wx:set_env(Env),
       %% Here you can do wx calls from your helper process.
       ...
    end),
...

在事件處理中,使用 connect 方法來連接要處理的事件,Erlang 以 receive 方式來接收處理指定的事件,這是最方便的事件處理方式。

示例程序結(jié)構(gòu)要點:

  • 導(dǎo)出入口函數(shù) -export([start/0]).;

  • 導(dǎo)入 wx 頭文件 -include_lib("wx/include/wx.hrl").;

  • 入口函數(shù)執(zhí)行時,執(zhí)行初始化;

    • 執(zhí)行 wx:new() 函數(shù)啟動 wx 服務(wù),初始化環(huán)境和內(nèi)存映射,可以傳入?yún)?shù)格式 {debug, Level};
    • 執(zhí)行 wx:batch() 以高效批量處理 wx 的各種命令,沒有它就不會處理 wxWidgets 線程的事件;
    • 執(zhí)行 create_window 創(chuàng)建窗體,并設(shè)置狀態(tài)欄 createStatusBar 再返回 Frame;
    • 執(zhí)行 wxFrame:connect() 連接各種事件處理,有標題欄的 close_window、 按鈕事件 command_menu_selected;
  • loop 函數(shù)中進入 receive 接收事件進行處理;

    • 注意 #wx{event=#wxClose{}} 這里的 wx 記錄體在 event 嵌套了 wxClose 記錄體,對應(yīng)了窗口的關(guān)閉事件。
    • Msg 等待處理一個消息事件,并繼續(xù)執(zhí)行 loop 循環(huán)。

事件元組格式如下,有 wxCommand 按鈕事件,有 窗體標題欄的關(guān)閉按鈕事件,根據(jù)需要進行匹配:

#wx{event=#wxClose{}}
#wx{obj=Frame, id=xxx, event=#wxCommand{}}
#wx{obj=Frame, event=#wxCommand{type=command_menu_selected}} 

可以給窗體設(shè)置圖標 code:which(?MODULE) 用來獲取當前模塊路徑:

Path = filename:dirname(code:which(?MODULE)),    
wxFrame:setIcon(Frame,  wxIcon:new(filename:join(Path,"sample.xpm"), [{type, ?wxBITMAP_TYPE_XPM}])),

XPM 是一個文本化圖像定義文件,格式如下:

/*XPM*/
static char * <pixmap_name>[] = 
{ 
<Values>
<Colors>
<Pixels>
<Extensions>
};

以下是 wxWidgets 的標準圖標:

/* XPM */
static const char * sample_xpm[] = {
    /* columns rows colors chars-per-pixel */
    "32 32 6 1",    // 定義一個 32*32 的圖像,它有 6 種顏色,每像素一個字符
    "  c black",    // 空格表示黑色,c 表示這種顏色是彩色模式
    ". c navy",     // . 表示海軍藍
    "X c red",      // X 表示紅色,除了命名的色彩表達,顏色值還可以使用十六進制 #ff0000 表達
    "o c yellow",   // o 表示黃色
    "O c gray100",  // O 表示灰色
    "+ c None",     // + 表示透明
    /* pixels */    // 下面是用色板上的顏色定義表示的像素
    "++++++++++++++++++++++++++++++++",
    "++++++++++++++++++++++++++++++++",
    "++++++++++++++++++++++++++++++++",
    "++++++++++++++++++++++++++++++++",
    "++++++++++++++++++++++++++++++++",
    "++++++++              ++++++++++",
    "++++++++ ............ ++++++++++",
    "++++++++ ............ ++++++++++",
    "++++++++ .OO......... ++++++++++",
    "++++++++ .OO......... ++++++++++",
    "++++++++ .OO......... ++++++++++",
    "++++++++ .OO......              ",
    "++++++++ .OO...... oooooooooooo ",
    "         .OO...... oooooooooooo ",
    " XXXXXXX .OO...... oOOooooooooo ",
    " XXXXXXX .OO...... oOOooooooooo ",
    " XOOXXXX ......... oOOooooooooo ",
    " XOOXXXX ......... oOOooooooooo ",
    " XOOXXXX           oOOooooooooo ",
    " XOOXXXXXXXXX ++++ oOOooooooooo ",
    " XOOXXXXXXXXX ++++ oOOooooooooo ",
    " XOOXXXXXXXXX ++++ oOOooooooooo ",
    " XOOXXXXXXXXX ++++ oooooooooooo ",
    " XOOXXXXXXXXX ++++ oooooooooooo ",
    " XXXXXXXXXXXX ++++              ",
    " XXXXXXXXXXXX ++++++++++++++++++",
    "              ++++++++++++++++++",
    "++++++++++++++++++++++++++++++++",
    "++++++++++++++++++++++++++++++++",
    "++++++++++++++++++++++++++++++++",
    "++++++++++++++++++++++++++++++++",
    "++++++++++++++++++++++++++++++++"
};

完整代碼,有改動:

%%%-------------------------------------------------------------------
%%% File    : hello.erl
%%% Author  : Matthew Harrison <harryhuk at users.sourceforge.net>
%%% Description : _really_ minimal example of a wxerlang app
%%%
%%% Created :  18 Sep 2008 by  Matthew Harrison <harryhuk at users.sourceforge.net>
%%%-------------------------------------------------------------------
-module(hello).

-include_lib("wx/include/wx.hrl").

-export([start/0]).

-define(menuID_TEST_QUIT,      400).
-define(menuID_TEST_CHECK,     401).
-define(menuID_TEST_RADIO_1,   402).
-define(menuID_TEST_RADIO_2,   403).
-define(menuID_TEST_RADIO_3,   404).


start() ->
    Wx = wx:new(),
    % Wx = wx:null(),
    Frame = wx:batch(fun() -> create_window(Wx) end),
    wxWindow:show(Frame),
    loop(Frame),
    wx:destroy().

create_window(Wx) ->
    Frame = wxFrame:new(Wx, 
            -1, % window id
            "Hello World", % window title
            [{size, {600,400}}]),


    wxFrame:createStatusBar(Frame,[]),

    MenuBar = wxMenuBar:new(?wxMB_DOCKABLE),
    create_test_menu(MenuBar),
    wxFrame:setMenuBar(Frame, MenuBar),

    %% if we don't handle this ourselves, wxwidgets will close the window
    %% when the user clicks the frame's close button, but the event loop still runs
    wxFrame:connect(Frame, close_window),
    wxFrame:connect(Frame, command_menu_selected), 

    ok = wxFrame:setStatusText(Frame, "Hello World!",[]),
    Frame.

create_test_menu(MenuBar) ->
    TestMenu   = wxMenu:new(),
    wxMenu:append(TestMenu, wxMenuItem:new([
            {id,    ?menuID_TEST_QUIT},
            {text,  "&Quit"},
            {help,  "Click to Exit..."}
            ])),
    wxMenu:appendSeparator(TestMenu), %% --------------------------
    %% note different way of adding check menu item
    wxMenu:appendCheckItem(TestMenu, ?menuID_TEST_CHECK,    "&Check item"),
    wxMenu:appendCheckItem(TestMenu, ?wxID_ABOUT,    "&About"),
    wxMenu:appendSeparator(TestMenu), %% --------------------------
    wxMenu:appendRadioItem(TestMenu, ?menuID_TEST_RADIO_1,  "Radio item &1"),
    wxMenu:appendRadioItem(TestMenu, ?menuID_TEST_RADIO_2,  "Radio item &2"),
    wxMenu:appendRadioItem(TestMenu, ?menuID_TEST_RADIO_3,  "Radio item &3"),
    wxMenuBar:append(MenuBar, TestMenu,     "&Test"),
    TestMenu.

loop(Frame) ->
    receive 
    #wx{event=#wxClose{}} ->
        io:format("~p Closing window ~n",[self()]),
        ok = wxFrame:setStatusText(Frame, "Closing...",[]),
        wxWindow:destroy(Frame),
        ok;

    #wx{obj=Frame, id=?menuID_TEST_QUIT, event=#wxCommand{}} = Wx->
        io:format("~p Quit now ~p ~n",[?MODULE, Wx]),
        wxWindow:destroy(Frame);

    #wx{obj=Frame, id=?wxID_ABOUT, event=#wxCommand{}} = Wx->
        io:format("~p About ~p ~n",[?MODULE, Wx]),
        dialog(?wxID_ABOUT, Frame),
        loop(Frame);

    #wx{obj=Frame, event=#wxCommand{type=command_menu_selected}} = Wx->
        io:format("~p Got ~p ~n",[?MODULE, Wx]),
        loop(Frame);

    Msg ->
        io:format("~p Got ~p ~n", [?MODULE, Msg]),
        loop(Frame)
    end.

dialog(?wxID_ABOUT,  Frame) ->
    Str = string:join(["Welcome to wxErlang.", 
               "This is the minimal wxErlang sample\n",
               "running under ",
               wx_misc:getOsDescription(),
               "."], 
              ""),
    MD = wxMessageDialog:new(Frame,
                 Str,
                 [{style, ?wxOK bor ?wxICON_INFORMATION}, 
                  {caption, "About wxErlang minimal sample"}]),

    wxDialog:showModal(MD),
    wxDialog:destroy(MD).

wxErlang gen_server

參考代碼來自 Erlang 源代碼中,wx 模塊提供的 Examples/simple/hello2.erl。

wx_object 提供了一個 start_link 方法來啟動一個 wx object server 對象服務(wù)器,它會自動在新進程中執(zhí)行模塊的 init 方法,Mod:init(Args) 并返回一個窗體對象:

start_link(Name, Mod, Args, Options) -> wxWindow() 

wx_object 不是 wxWidgets 的類,而是 wx 在內(nèi)存里的具體物理實現(xiàn),可以看做是 Erlang 中 gen_server 的 behaviour。

當然,現(xiàn)在是用 Erlang,還是要用它的說法,用戶程序模塊應(yīng)該導(dǎo)出以下函數(shù):

  • init(Args)
  • handle_call(Msg, {From, Tag}, State)
  • handle_event(#wx{}, State)
  • handle_info(Info, State)

這樣的模塊定義,實現(xiàn)這些函數(shù),就完成了 wx 對象服務(wù)器的結(jié)構(gòu)定義。事件發(fā)生時,就會執(zhí)行相應(yīng)的模塊方法。

示例代碼如下,有改動:

%%%-------------------------------------------------------------------
%%% File    : hello.erl
%%% Author  : Matthew Harrison <harryhuk at users.sourceforge.net>
%%% Description : _really_ minimal example of a wxerlang app
%%%               implemented with wx_object behaviour
%%%
%%% Created :  18 Sep 2008 by  Matthew Harrison <harryhuk at users.sourceforge.net>
%%%            Dan rewrote it to show wx_object behaviour
%%%-------------------------------------------------------------------
-module(hello2).
-include_lib("wx/include/wx.hrl").

-export([start/0,
         init/1, handle_info/2, handle_event/2, handle_call/3,
         code_change/3, terminate/2]).

-behaviour(wx_object).

-record(state, {win}).

start() ->
    wx_object:start_link(?MODULE, [], []),
    loop().

%% Init is called in the new process.
init([]) ->
    wx:new(),
    Frame = wxFrame:new(wx:null(), 
            -1, % window id
            "Hello World", % window title
            [{size, {600,400}}]),
    
    wxFrame:createStatusBar(Frame,[]),

    %% if we don't handle this ourselves, wxwidgets will close the window
    %% when the user clicks the frame's close button, but the event loop still runs
    wxFrame:connect(Frame, close_window),
    
    ok = wxFrame:setStatusText(Frame, "Hello World!",[]),
    wxWindow:show(Frame),
    {Frame, #state{win=Frame}}.

loop() ->
    receive 
        {'EXIT',_,_}->
            io:fwrite("Exit...");
        Msg ->
            io:fwrite("Loop ~p...~n", [Msg]),
            loop()
    end.

%% Handled as in normal gen_server callbacks
handle_info(Msg, State) ->
    io:format("Got Info ~p~n",[Msg]),
    {noreply,State}.

handle_call(Msg, _From, State) ->
    io:format("Got Call ~p~n",[Msg]),
    {reply,ok,State}.

%% Async Events are handled in handle_event as in handle_info
handle_event(#wx{event=#wxClose{}}, State = #state{win=Frame}) ->
    io:format("~p Closing window ~n",[self()]),
    ok = wxFrame:setStatusText(Frame, "Closing...",[]),
    wxWindow:destroy(Frame),
    {stop, normal, State}.

code_change(_, _, State) ->
    {stop, not_yet_implemented, State}.

terminate(_Reason, _State) ->
    ok.

wxErlang Sudoku

在 Erlang 源代碼中,wx 模塊提供了 Examples,其中有 sudoku 游戲的示范。

Sudoku 是日語的數(shù)獨,最簡單的數(shù)獨就是九宮格。標準 Sudoku 從整體上看,是 9 X 9 的盤格,每 3 X 3 的盤格作為一區(qū)。

Sudoku 的游戲規(guī)則非常簡單,全盤的每一行、每一列,必須填進 9 個數(shù)字。每行每列的數(shù)字,必須完全不同,不允許出現(xiàn)重復(fù)數(shù)字,每個小區(qū) 3 X 3 的盤格也不允許出現(xiàn)重復(fù)數(shù)字。

Sudoku 這個程序也是基于 Erlang/OTP 工程的基本框架,即 Supervision Tree 監(jiān)督樹架構(gòu)實現(xiàn)的,大量使用了 Erlang/OTP 四大 Behaviour 中的 gen_server。除 Supervisor 外,它們都在監(jiān)督樹充當 Worker 角色:

  • gen_server Generic server behaviour,實現(xiàn) C/S 架構(gòu)中的服務(wù)端;
  • gen_statem Generic state machine behaviour,實現(xiàn)一個有限狀態(tài)機 FSM - Finite State Machine;
  • gen_event Generic event handling behavior,實現(xiàn)事件處理功能;
  • supervisor Generic supervisor behavior,實現(xiàn)監(jiān)督者,它以監(jiān)督樹的方式存在;

Sudoku 模塊分解:

  • sudoku.hrl

    引入 wx 頭文件,定義結(jié)構(gòu)體和菜單常量,還有一個宏 TC 包裝參數(shù)方便使用主模塊中的 tc 方法;

  • sudoku.erl

    是程序主進程模塊,定義兩個入口 gostart,還有兩個內(nèi)部方法:

    • init 初始化,執(zhí)行 sudouku_gui:new 設(shè)置界面,同時執(zhí)行 sudoku_game:init(GFX) 初始化游戲邏輯模塊,并進入消息循環(huán)。GFX 是整個游戲的 GUI 界面模塊,即 sudoku_gui 模塊,它通過消息發(fā)送給主模塊,Game ! {gfx, self()}
    • tc 封裝 timer:tc 函數(shù),即 Time counter 計時器,用來測量運行時間;
  • sudoku_gui.erl GUI 界面模塊,只供 sudoku.erl 調(diào)用;

    在唯一的對外接口 new 函數(shù)中執(zhí)行 wx_object:start_link 開始一個類似 gen_server behaviour 的服務(wù)器,配置主界面及事件處理,使用 wxBoxSizer 做布局,將主界面分成 TopMain 兩塊。Top 部分放按鈕,Main 用來放棋盤,通過執(zhí)行 sudoku_board:new 來初始化棋盤。在 handle_info 函數(shù)中接收來自棋盤消息,如 set_val 設(shè)置格子數(shù)值消息,并通過 validate 消息通知主模塊。

  • sudoku_board.erl 是游戲棋盤實現(xiàn):

    屬于 GUI 功能的一部分,棋盤顯示是通過 wxDC 繪圖實現(xiàn)的,每個格子只占 Canvas 中的一個繪圖區(qū)域。棋盤模塊也是標準 gen_server,綁定的各種事件,如鍵盤事件,按數(shù)字鍵就可以在格子上填數(shù)字,在 handle_eventhandle_sync_event 回調(diào)函數(shù)中處理各種事件,如窗口大小變化事件 #wxSize 中就執(zhí)行 redraw 重繪格子,draw_board 函數(shù)就是真正畫格子方法。在鼠標移動過程中或鍵盤事件中,通過鼠標位置計算是哪個格子,然后再向 sudoku_gui 發(fā)送 set_val 消息。

      wxWindow:connect(Win, paint,  [callback]),
      wxWindow:connect(Win, size,  []),
      wxWindow:connect(Win, erase_background, []),
      wxWindow:connect(Win, key_up, [{skip, true}]),
      wxWindow:connect(Win, left_down, [{skip, true}]),
      wxWindow:connect(Win, enter_window, [{skip, true}]),
    
      handle_sync_event( #wx{event=#wxPaint{}} ...
      handle_event( #wx{event=#wxKey{keyCode=KeyC}} ...
      handle_event( #wx{event=#wxMouse{type=left_down,x=X,y=Y}} ...
      handle_event( #wx{event=#wxSize{}} ...
    
  • sudoku_gamr.erl 是數(shù)獨游戲邏輯模塊,由 sudoku 模塊執(zhí)行初始化,init(GFX) 函數(shù)傳入的 GUI 界面模塊用于消息傳遞,整個程序的消息循環(huán)就是這個核心模塊。注意,它和入口模塊是同進程的,并不像 sudoku_guisudoku_board 是 wx_object behaviour,會創(chuàng)建新進程運行,它們和主進程的通信走消息管道。所以進入消息循環(huán)后,主要的邏輯就是主線程與 GUI 線程的消息處理,以及根據(jù)游戲規(guī)則響應(yīng)。列如,validate 消息是在給格子設(shè)置數(shù)字時產(chǎn)生的,核心模塊需要對數(shù)字進行判斷,如果是相同的數(shù)字就忽略繼續(xù)消息循環(huán),否則進入 validate 函數(shù)驗證,再將驗證結(jié)果發(fā)送消息給 GUI 模塊進行相應(yīng)的繪圖。

GUI 界面在關(guān)閉時,發(fā)送了一條消息 G ! quit 并給 wx_object 返回一個 {stop, shutdown, S},這個返回值不符合 wx_object 的要求,會觸發(fā)異常導(dǎo)致不能正常關(guān)閉程序,應(yīng)該在關(guān)閉時返回 {stop, Reason, State}

handle_event(#wx{}, State) should return
{noreply, State} | {noreply, State, Timeout} | {stop, Reason, State}

同時 G ! quit 消息在主模塊消息循環(huán)中會轉(zhuǎn)換為一個 halt 原子類型,主模塊的入口設(shè)置的 case 條件不匹配又會導(dǎo)致異常:

init(Halt) ->
    ?TC(sudoku_gui:new(self())),
    % tc(fun() -> sudoku_gui:new(self()) end, ?Module, ?Line)
    receive {gfx, GFX} -> ok end,
    case sudoku_game:init(GFX) of
        Halt -> erlang:halt();
        Stop -> exit(Stop)
    end.

這里示例中的 BUG 代碼,需要作相應(yīng)修改。

draw_board 格子繪制的相關(guān)代碼:

-record(state, {win, parent, board=[], pen, fonts=[]}).

init([ParentObj, ParentPid]) ->
    ...
    %% Init pens and fonts
    Pen = wxPen:new({0,0,0}, [{width, 3}]),
    Fs0  = [{Sz,wxFont:new(Sz, ?wxSWISS, ?wxNORMAL, ?wxNORMAL,[])} ||
           Sz <- [8,9,10,11,12,13,14,16,18,20,22,24,26,28,30,34,38,42,44,46]],
    TestDC  = wxMemoryDC:new(),
    Bitmap = wxBitmap:new(256,256),
    wxMemoryDC:selectObject(TestDC, Bitmap),
    true = wxDC:isOk(TestDC),
    CW = fun({Sz,Font},Acc) ->
         case wxFont:ok(Font) of
             true ->
             wxDC:setFont(TestDC, Font),
             CH = wxDC:getCharHeight(TestDC),
             [{CH,Sz,Font} | Acc];
             false ->
             Acc
         end
     end,
    Fs = lists:foldl(CW, [], Fs0),
    wxMemoryDC:destroy(TestDC),
    {Win, #state{win=Win, board=[], pen=Pen, fonts=Fs,parent=ParentPid}}.

redraw(S = #state{win=Win}) ->
    DC0  = wxClientDC:new(Win),
    DC   = wxBufferedDC:new(DC0),
    Size = wxWindow:getSize(Win),
    redraw(DC, Size, S),
    wxBufferedDC:destroy(DC),
    wxClientDC:destroy(DC0),
    ok.

redraw(DC, Size, S) ->    
    wx:batch(fun() -> 
             wxDC:setBackground(DC, ?wxWHITE_BRUSH),
             wxDC:clear(DC),
             BoxSz = draw_board(DC,Size,S),
             F = sel_font(BoxSz div 3,S#state.fonts),
             [draw_number(DC,F,BoxSz,Sq) || Sq <- S#state.board]
         end).

draw_number(DC,F,Sz,#sq{key={R,C},val=Num,given=Bold,correct=Correct}) ->
    {X,Y} = get_coords(Sz,R-1,C-1),
    TBox = Sz div 3,
    if Bold -> 
        wxFont:setWeight(F,?wxBOLD),
        wxDC:setTextForeground(DC,{0,0,0});
       Correct =:= false ->
        wxFont:setWeight(F,?wxNORMAL),
        wxDC:setTextForeground(DC,{255,40,40,255});
       true ->
        wxFont:setWeight(F,?wxNORMAL),
        wxDC:setTextForeground(DC,{50,50,100,255})
    end,
    wxDC:setFont(DC,F),
    CH = (TBox - wxDC:getCharHeight(DC)) div 2,
    CW = (TBox - wxDC:getCharWidth(DC)) div 2,
    wxDC:drawText(DC, integer_to_list(Num), {X+CW,Y+CH+1}),
    ok.

draw_board(DC,{W0,H0},#state{pen=Pen}) ->
    BoxSz = getGeomSz(W0,H0),
    BS = ?BRD+3*BoxSz,

    wxPen:setWidth(Pen, 3),
    wxPen:setColour(Pen, {0,0,0}),
    wxDC:setPen(DC,Pen),
    
    wxDC:drawRoundedRectangle(DC, {?BRD,?BRD,3*BoxSz+1,3*BoxSz+1}, 
                  float(?ARC_R)),
    %% Testing DrawLines
    wxDC:drawLines(DC, [{?BRD+BoxSz, ?BRD}, {?BRD+BoxSz, BS}]),
    wxDC:drawLine(DC, {?BRD+BoxSz*2, ?BRD}, {?BRD+BoxSz*2, BS}),
    wxDC:drawLine(DC, {?BRD, ?BRD+BoxSz}, {BS, ?BRD+BoxSz}),
    wxDC:drawLine(DC, {?BRD, ?BRD+BoxSz*2}, {BS, ?BRD+BoxSz*2}),

    %% Draw inside lines
    wxPen:setWidth(Pen, 1),
    wxDC:setPen(DC,Pen),
    TBox = BoxSz div 3,   
    wxDC:drawLine(DC, {?BRD+TBox, ?BRD}, {?BRD+TBox, BS}),
    wxDC:drawLine(DC, {?BRD+TBox*2, ?BRD}, {?BRD+TBox*2, BS}),
    wxDC:drawLine(DC, {?BRD+TBox+BoxSz, ?BRD}, {?BRD+TBox+BoxSz, BS}),
    wxDC:drawLine(DC, {?BRD+TBox*2+BoxSz, ?BRD}, {?BRD+TBox*2+BoxSz, BS}),
    wxDC:drawLine(DC, {?BRD+TBox+BoxSz*2, ?BRD}, {?BRD+TBox+BoxSz*2, BS}),
    wxDC:drawLine(DC, {?BRD+TBox*2+BoxSz*2, ?BRD}, {?BRD+TBox*2+BoxSz*2, BS}),
    %% Vert
    wxDC:drawLine(DC, {?BRD, ?BRD+TBox}, {BS, ?BRD+TBox}),
    wxDC:drawLine(DC, {?BRD, ?BRD+TBox*2}, {BS, ?BRD+TBox*2}),
    wxDC:drawLine(DC, {?BRD, ?BRD+TBox+BoxSz}, {BS, ?BRD+TBox+BoxSz}),
    wxDC:drawLine(DC, {?BRD, ?BRD+TBox*2+BoxSz}, {BS, ?BRD+TBox*2+BoxSz}),
    wxDC:drawLine(DC, {?BRD, ?BRD+TBox+BoxSz*2}, {BS, ?BRD+TBox+BoxSz*2}),
    wxDC:drawLine(DC, {?BRD, ?BRD+TBox*2+BoxSz*2}, {BS, ?BRD+TBox*2+BoxSz*2}),
    BoxSz.

繪制格式步驟:

  • wxDC:drawRoundedRectangle 繪制一個圓角大矩形;
  • wxDC:drawLine 繪制大、小九宮格的分割線,只是粗細的區(qū)別;
  • wxDC:drawText 在 draw_number 函數(shù)中繪制數(shù)字;

注意 redraw 方法中的 [draw_number(DC,F,BoxSz,Sq) || Sq <- S#state.board] 表達,它是列表推理 List Comprehensions,相當于枚舉了各個數(shù)字并調(diào)用 draw_number 函數(shù)進行繪制。

Makefile 自動編譯工具

代碼變成可執(zhí)行文件,叫做編譯 compile,工程中通過要編譯多個文件,整體叫做構(gòu)建 build。

Make 是最常用的構(gòu)建工具,誕生于 1977 年,主要用于 C 語言的項目。但是實際上 ,任何只要某個文件有變化,就要重新構(gòu)建的項目,都可以用 Make 構(gòu)建。

GNU 的 Make 工具可以替代手工的編譯工作,通過 Makefile 腳本實現(xiàn)工程級別的編譯工作自動化。

列如,以下一個 Makefile:

.SUFFIXES: .erl .beam
 
.erl.beam:
    erlc -W $<
ERL = erl -boot start_clean 
 
MODS = hello shop

all: compile 
 
compile: ${MODS:%=%.beam}
    @echo "make clean - clean up"
 
clean:  
    rm -rf *.beam erl_crash.dump 

保存到源代碼 hello.erl、shop.erl 同一文件夾下,執(zhí)行 erl -make,編譯成功就會出現(xiàn)源代碼對應(yīng)的 .beam。

在 Windows 系統(tǒng)使用 Gnu make 命令,需要 ComSpec 這個環(huán)境變量指向 cmd.exe,或者設(shè)置 SHELL=cmd.exe 否則 shell 會執(zhí)行失敗:

process_begin: CreateProcess(NULL,gcc -c test.c, ...)failed. 
make(e=2): 系統(tǒng)找不到指定的文件 
make:*** [test.o] 錯誤2 

列如,Erlang 源代碼中提供了 wxErlang 模塊的示例,其編譯腳本 otp_src_23.0\lib\wx\examples\demo\Makefile 是為 Linux 系統(tǒng)準備的,在 Windows 系統(tǒng)上使用需要修改一下;

SHELL=cmd.exe

ERL_TOP = ..\..\..\..
TOPDIR   = ..\..
SRC = .
BIN = .
ERLINC = $(TOPDIR)/include
ERLC = erlc
TESTMODS = \
    demo \
    demo_html_tagger \
    ...
    ex_graphicsContext

TESTTARGETS = $(TESTMODS:%=%.beam)
TESTSRC = $(TESTMODS:%=%.erl)

# Targets
$(TESTTARGETS):$(TESTSRC)
opt debug:  $(TESTTARGETS)
    ERLC -o $(TOPDIR)/ebin  $(TESTSRC)
clean:
    del $(TOPDIR)\ebin\*.beam
    del "$(TOPDIR)\ebin\erl_crash.dump"
#   del $(TESTTARGETS:%="$(TOPDIR)/ebin/%")
#   rm -f $(TESTTARGETS)
#   rm -f *~ core erl_crash.dump

# docs:

run: opt
    erl -smp -detached -pa $(TOPDIR)\ebin -s demo

然后執(zhí)行編譯,運行測試:

$ make
$ erl -noshell -s demo start -s init stop

make 命令只是一個根據(jù)指定的 Shell 命令進行構(gòu)建的工具,它的規(guī)則很簡單:

  • Target 規(guī)定要構(gòu)建哪個文件,用什么命令;
  • Dependence 它依賴哪些源文件;
  • Update 當那些文件有變動時,如何重新構(gòu)建它。

構(gòu)建規(guī)則都寫在 Makefile 文件里面,這個文件由一系列規(guī)則 rules 構(gòu)成:

<target> : <prerequisites> 
[tab]  <commands>
  • 第一行冒號前面的部分,叫做目標 Target,多目標用空格隔開,冒號后面的部分叫做前置條件 prerequisites。
  • 第二行必須由一個 tab 鍵起首,后面跟著命令 commands。
  • 目標是必需的,不可省略,前置條件和命令都是可選的,但是兩者之中必須至少存在一個。
  • 每條規(guī)則就明確兩件事:構(gòu)建目標的前置條件是什么,以及如何構(gòu)建。

目標通常是文件名,指明 Make 命令所要構(gòu)建的對象,除了文件名,目標還可以是某個操作的名字,這稱為偽目標 phony target。

在定義目標時,如果當前目錄中,正好有一個文件同名,比如,目標叫做 clean,Make 執(zhí)行時發(fā)現(xiàn) clean 文件已經(jīng)存在,而且是最新的狀態(tài),就認為沒有必要重新構(gòu)建了,就不會執(zhí)行指定的命令。為了避免這種情況,可以明確聲明 clean 是偽目標,寫法如下:

.PHONY: clean
clean:
        rm *.o temp

聲明 clean 是偽目標之后,make 就不會去檢查是否存在一個叫做 clean 的文件,而是每次運行都執(zhí)行對應(yīng)的命令。像 .PHONY 這樣的內(nèi)置目標名還有不少,偽目標以句點開頭跟大寫字母,可以查看手冊。

前置條件通常是一組文件名,之間用空格分隔。它指定了目標是否重新構(gòu)建的判斷標準: 只要有一個前置文件不存在,或者有過更新,前置文件的 last-modification 時間戳比目標的時間戳新,目標就需要重新構(gòu)建。

命令 commands 表示如何更新目標文件,由一行或多行的 Shell 命令組成。它是構(gòu)建目標的具體指令,它的運行結(jié)果通常就是生成目標文件。

Make 有隱含規(guī)則 implict rule,比如:

foo : foo.o bar.o
        cc -o foo foo.o bar.o $(CFLAGS) $(LDFLAGS)

上面的規(guī)則中,沒有定義 foo.o 目標,make 會自動使用隱含規(guī)則,選檢查 foo.o 文件是不存在,然后檢查目錄下對應(yīng)的源代碼,比如 foo.c 文件就會執(zhí)行 C 編譯器,如果是 foo.p 文件則執(zhí)行 Pascal 編譯器,如此。

隱含規(guī)則和隱含變量是配套的,C compiler,對應(yīng)的隱含變量就是 cc 命令,可以直接調(diào)用,(CC)、(CFLAGS)、$(CPPFLAGS) 等。

Make 的一些編程能力:

  • Make 支持命令換行,在換行符前加反斜杠 \ 轉(zhuǎn)義,$$ 表示轉(zhuǎn)義 $ 符號。

  • 井號 # 在 Makefile 中表示其后面的內(nèi)容是注釋。

  • 支持 *、?[...] 通配符用來指定一組符合條件的文件名。

  • 支持匹配符,%,如 %.o: %.c 為當前目錄下源碼文件定義相應(yīng)的目標。

  • 支持變量,如 v1 = Hi! 定義了 v1 變量,${v1}$(v1) 使用變量,例如 @echo $(v1),或者 v2 = $(v1)。

  • 變量高級引用,$(var:a=b) 或者 ${var:a=b},例如以下 bar 變量最后的值是 a.c b.c l.a c.c

      foo := a.o b.o l.a c.o
      bar := $(foo:.o=.c)
    
  • 內(nèi)置變量,如$(CC) 指向當前使用的編譯器,$(MAKE) 指向當前使用的 Make 工具。

  • 自動變量:

    • $@ 指代當前 Make 命令當前構(gòu)建的那個目標。
    • $< 指代第一個前置條件。
    • $? 指代比目標更新的所有前置條件,之間以空格分隔。比如,規(guī)則為 t: p1 p2,其中 p2 的時間戳比 t 新,$? 就指代 p2。
    • $^ 指代所有前置條件,之間以空格分隔。
    • $* 指代匹配符 % 匹配的部分, 比如 % 匹配 f1.txt 中的 f1,$* 就表示 f1。
    • $(@D)$(@F) 分別指向 $@ 自動變量的目錄名和文件名部分。
    • $(<D)$(<F) 分別指向 $< 自動變量的目錄名和文件名部分。
  • 支持 if-else 條件判斷結(jié)構(gòu):

      ifeq ($(CC),gcc)
          libs=$(libs_for_gcc)
      else
          libs=$(normal_libs)
      endif
    
  • 支持循環(huán)結(jié)構(gòu):

      LIST = one two three
    
      all:
          for i in $(LIST); do \
              echo $$i; \
          done
    
      # 等同于
    
      all:
          for i in one two three; do \
              echo $i; \
          done
    
  • 支持使用函數(shù):

      $(function arguments)
      # 或者
      ${function arguments}
    

Makefile 提供了許多內(nèi)置函數(shù),可供調(diào)用。下面是幾個常用的內(nèi)置函數(shù)。

Text Functions

格式 示范
$(subst from,to,text) $(subst ee,EE,feet on the street)
$(lastword names…) $(lastword foo bar)
$(patsubst pattern,replacement,text) $(patsubst %.c,%.o,x.c.c bar.c)
$(strip string) $(strip a b c )
$(findstring find,in) $(findstring a,a b c)
$(filter pattern…,text) (filter %.c %.s,(sources))
$(sort list) $(sort foo bar lose)
$(word n,text) $(word 2, foo bar baz)
$(wordlist s,e,text) $(wordlist 2, 3, foo bar baz)

File Name Functions

格式 示范
$(dir names…) $(dir src/foo.c hacks)
$(notdir names…) $(notdir src/foo.c hacks)
$(suffix names…) $(suffix src/foo.c src-1.0/bar.c hacks)
$(basename names…) $(basename src/foo.c src-1.0/bar hacks)
$(addsuffix suffix,names…) $(addsuffix .c,foo bar)
$(addprefix prefix,names…) $(addprefix src/,foo bar)
$(join list1,list2) $(join a b,.c .o)
$(wildcard pattern)
$(realpath names…)
$(abspath names…)

Conditional Functions

格式 示范
$(if condition,then-part[,else-part])
$(or condition1[,condition2[,condition3…]])
$(and condition1[,condition2[,condition3…]])

Make Control Functions

格式 示范
$(error text…) (error error is(ERROR1))
$(info text…)
$(warning text…)

其它函數(shù)

函數(shù) 格式 作用
Foreach Function $(foreach var,list,text) Repeat some text with controlled variation.
File Function $(file op filename[,text]) Write text to a file.
Call Function $(call variable,param,param,…) Expand a user-defined function.
Value Function $(value variable) Return the un-expanded value of a variable.
Eval Function (eval(call PROGRAM_template,$(prog)) Evaluate the arguments as makefile syntax.
Origin Function $(origin variable) Find where a variable got its value.
Flavor Function $(flavor variable) Find out the flavor of a variable.
Shell Function $(shell echo *.c) Substitute the output of a shell command.
Guile Function Use GNU Guile embedded scripting language.

腳本模板 Makefile.template:

# leave these lines alone
.SUFFIXES: .erl .beam .yrl

.erl.beam:
    erlc -W $<

.yrl.erl:
    erlc -W $<

ERL = erl -boot start_clean

# Here's a list of the erlang modules you want compiling
# If the modules don't fit onto one line add a \ character
# to the end of the line and continue on the next line
# Edit the lines below

MODS = module1 module2 \
    module3 ... special1 \
    ...
    moduleN

# The first target in any makefile is the default target.
# If you just type "make" then "make all" is assumed (because
# "all" is the first target in this makefile)

all: compile

compile: ${MODS:%=%.beam} subdirs

## special compilation requirements are added here

special1.beam: special1.erl
    ${ERL} -Dflag1 -W0 special1.erl

## run an application from the makefile

application1: compile
    ${ERL} -pa Dir1 -s application1 start Arg1 Arg2

# the subdirs target compiles any code in sub-directories

subdirs:
    cd dir1; $(MAKE)
    cd dir2; $(MAKE)
    ...

# remove all the code

clean:
    rm -rf *.beam erl_crash.dump
    cd dir1; $(MAKE) clean
    cd dir2; $(MAKE) clean

最重要的是:

MODS = module1 module2 module3 ... special1 ...

它定義了需要編譯的目標模塊,然后使用 ${MODS:%=%.beam} 轉(zhuǎn)換成 beam 擴展名,執(zhí)行 make 可以指定編譯的目標:

make [Target]

就會將模塊編譯生成腳本定義目標文件。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容