Erlang 图形编程 - wxErlang GUI

wxErlang GUI

wxErlang GUI

Erlang 这门编程语言通常用于服务器方面,虽然它也有类似 Wings 3D 这样图像密集的应用。wxWidget 是对 Erlang 支持最好的图像 API,它为 GUI 编程提供一个大型,成熟,稳定的跨平台 API。

在这个部分,假设你早已:

  • 安装了 Erlang
  • 已经知道怎么使用 Erlang shell
  • 有用过合适的文本编辑器写程序

并非所有wx调用都产生一个直观的图形显示;在 Erlang shell 中,通常你只能看到返回值。这些值可能是神秘的,尤其是如果你过去没有用过 Erlang。因为 Erlang 是一门函数式编程语言,所以每个wx调用都会返回一个值。这些值大多是 tuple。而这些 tuple 又大多有记录 record 的内容。在记录格式下理解 wx 返回值会更容易。Erlang shell 需要被告知 wx 的定义。

寻找在你系统上 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 调用都应该返回一系列模块,如果 rr 调用出错,你将得到空列表。 打开你的文本编辑器。把上面三行代码复制粘贴到一个临时文件。然后,当你开始一个新小节,或者重新开始小节,或者因为崩溃不得不重启,你可以把它们复制粘贴到 shell 里。现在就试试,确保它们能正常工作。退出 Erlang,重启,然后复制粘贴这些代码到 shell。

很不幸,我们不能在 shell 中使用 Erlang 宏定义。不过这也是另一个需要定位 wx.hrl 文件的理由:为了在 shell 中使用,我们得查询需要的 wx 宏符号所对应的值。

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

它运行了吗?可能有消息说对称多处理 SMP 没有开启,或者“SMP emulator required”。在一些 Windows Erlang 发行版中 SMP 没有默认开启。退出 Erlang shell 然后带参 -smp 重启,在DOS命令行中就像这样: werl.exe -smp 如果 wx:new() 正常运行,将会返回一个记录。

在 wxWidgets 中,一个窗口相当于一个 frame。让我们写一个简单的程序,然后添加它。输入下面的代码可以生成一个 frame:

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

但是屏幕上没有任何改变。为什么?我们必须提出想看看frame的请求它才会出现。输入:

wxFrame:show(F).

它会返回 true,并且你就会看到一个窗体。

从 shell 异常中恢复

只需要点击关闭按钮就能关闭 frame。但是别那么做,先试试下面这个无意义的调用;

nothing:doint().

这会让 frame 消失,随之出现的还有异常错误消息。这是因为 wxWidgets 在它 shell 的进程运行图形程序,如果 shell 中出现异常又不捕获,它就会当即被杀死。

仅仅键盘输入错误就有可能导致 GUI 完全丢失,这种情况而且会经常发生。无论在哪只要你引发了错误就要重新输入一遍。没人愿意来上几次吧?所以,在开始教程之前,输入下面的代码:

catch_exception (true).

现在休息一下吧!这样设置后可以让GUI程序无视引发错误的地方继续工作。把上面的代码全都放到一个临时文件,像这样:

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 在屏幕上现实时,输入:

wxFrame:destory(F).

它应该返回 ok 然后 frame 销毁消失了。

StatusBar 状态栏

就当是开心一下,创建多个frame:

  • 创建一个标题为"Hey!"的wxFrame,变量名为 F1
  • 显示 F1
  • 创建一个标题为"Boo!"的wxFrame,变量名为 F2
  • 显示 F2.
  • 使用 wxFrame:destroy 将两个 frame 销毁。

别把这些代码放到临时文件,我们的 lesson 要从第一个 destroy 调用继续。

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).

状态栏不仅方便程序功能,也便于调试。

wxFrame:createStatusBar(F).

现在你的 frame 就会增加一个状态栏。

将一些文字放到状态栏中:

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

花一点时间把上面这些代码复制粘贴到你的临时文件,尝试向状态栏设置其他文字,然后恢复为“Quiet here”:

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

现在应该已经回到了之前你向状态栏添加文字的样子。

Menu 菜单栏

按照惯例 wxWidgets 中的 frame 都会有一个菜单栏。这样看起来状态栏菜单栏没什么区别。然而,菜单栏通常由其他东西组成:它们需要被组合到一起。

在 wxWidgets 中,复杂的东西通常都是由简单的东西开始一步步构建的。wxWidgets 的 API 不假设新建的复杂的东西包含任何简单的东西。对于越复杂的东西,所需的构建步骤就越多。

让我们尽快生成一个可见的菜单栏。当你完成后记得复制下面的代码到你的临时文件,

生成一个菜单栏,输入:

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

但是 frame 仍然没有菜单栏吧?我们有看到菜单栏关联 frame 吗?是的,F 是到目前为止你仅有的 frame,但 wx 不假设你想把 MenuBar 放到 F 里面去。

尝试一下将 MenuBar 设置为 F 的一部分:

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

它可能会返回ok,但是...窗口还是没有任何东西!的确发生了菜单栏和 frame 的关联。问题是:frame 显示这个已经关联的菜单栏并没有菜单项,我们得做点什么。

下面几步将添加菜单项到菜单栏,然后显示它。

大多数 GUI 应用程序都有一个 File(文件)菜单。输入这个:

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

又是这样,wxWidgets 不知道你想把这个菜单添加到哪,所以不会显示。你必须告诉 FileMn 它应该被放到哪个菜单栏。现在我们把 FileMn 放到 F 的菜单栏 MenuBar 里:

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

“&File” 前面的 “&” 符号表示你可以输入快捷键 Alt-F 使用它。

现在有菜单了,但是一个好的菜单没有菜单项怎么行。点击 File 菜单或者 Alt-F,好吧,没有任何东西,那是怎么回事?

需要添加一个菜单项,每个 File 菜单都应该有一个 Quit 菜单项,让我们也添加一个,并添加到文件菜单上,输入:

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

现在,点击 File 菜单,可以看到 Quit 菜单项了。

回顾上述设置的菜单的所有代码,你会看到:

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).

我们还可以添加什么呢?每一个得体的应用程序都有一个 Help 菜单。然后 Help 菜单通常有一个 About 菜单项。

重复你之前添加 File 菜单所用的 new append 命令:

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

添加 About 菜单后,同样重复之前将 Quit 菜单项添加到 File 菜单的步骤,就会得到另外一个 Help 菜单。点一下 Help 菜单,你会看到 About 菜单。

花一点时间把代码复制粘贴到你的临时文件。

Events 事件

到目前为止,我们所做的都没有涉及事件。你可能认为 Erlang wxWidgets 没有事件。如果你现在输入 flush()., 你就不会那样想了。 事实上,在 wxWidgets 中每个鼠标点击都会触发事件。它们被wx以默认的一些方式处理。通常,wx 的默认处理方式是忽略它们。让我们捕获事件,看看它到底是什么样的。

使用 connect 联接并查看事件,输入:

wxFrame:connect (F, close_window).

点击 frame 上的关闭按钮,然后输入:

flush().

你会看到这样的输出:

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

注意,现在点击关闭按钮不会真正的关闭一个 frame。你将重写这个默认行为。 多点几次关闭按钮,然后点击最大化窗口按钮,最小化窗口按钮。然后再次输入 flush().。你会看到 close_window 事件,但是没有最大化最小化事件。

同样请注意 shell 怎样输出它收到的事件:它不会使用之前读取的 wx 定义。你只会看到原始 tuple。这使得我们知晓这些 wx 事件是什么的难度增加。

有一个使用记录定义查看事件的方法。下面的 fun 返回一个 receive 接收到的事件,输入:

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

点击关闭按钮,然后调用事件读取器:

Ev().

你会看到类似这样的东西:

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

让我们尝试关联 connect 菜单选择事件,输入:

wxFrame:connect (F, command_menu_selected).

尝试选择菜单。选择File->Quit,然后选择File->About。然后输入Ev().看看生成了哪些事件。除了id外,返回的事件应该都是一样的。

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

知道发生了什么事件很有用,有时它有助于观察细节。但是大多数时候,我们只想当事件发生时做出我们希望的动作。所以我们必须捕获事件,然后搞懂怎样给它添加一个动作。

wx 中的事件由回调函数处理。首先,生成一个回调函数。输入:

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

试试,给它传入正确的参数。

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

它会响铃吗?会的。

现在将它关联到你的 frame 的 close_windows 事件上:

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

再试试点击关闭按钮,就会有哔哔声。试试调用 Ev(). 它不再返回 close_window 事件。

因为简单的哔哔声对于这个关闭窗口事件是没有实际意义的,你可能想解除关联

wxFrame:disconnect (F, close_window).

Dialog 对话框

一个“About”菜单项应该给我们显示一个模态对话框。但是怎样生成这个对话框?这里是最简单的方法。

生成一个模态对话框,输入下面这行代码:

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

它应该返回一个类似 #wx_ref 这样的回应,但是屏幕上不会有任何显示。

要想显示对话框并与之交互,输入:

wxMessageDialog:showModal (D).

在你的屏幕上就会看到弹出的对话框。

因为对话框是模态的,所以直到你点 OK 之前 shell 都不会有任何返回值。返回值应该是 5100。如果你看看 wx.hrl,你就会知道它代表 wxID_OK

wxErlang Hello

参考代码来自 Erlang 源代码中,wx 模块提供了 Examples。

这是一个基本窗体演示,可以通过以下命令编译执行:

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

不像专门为特定系统设计的快速 GUI 原型程序,如 Tcl/Tk,从 Erlang shell 命令行开始图形界面开发之前有一些准备工作要做。

注意 Erlang 是面向函数式的编程,和面向对象的编程在表达式上有些差别,如以下 OOP 代码:

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

Erlang 对应的代码:

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

很多对象模块都提供了 destroy 解构函数,这本来在 OOP 中是对象的析构函数,在 Erlang 中则就模块函数的方式提供。

对于 wxWidgets 那些非类实现的方法,在 Erlang 中使用 wx_misc 模块实现。
wxWidgets 对象和 Erlang 的对应参考:

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 时,需要刻意调用 wx:new() 来执行 GUI 程序的初始化,处理环境变量和内存映射。为了在多线程中共用这些配置,需要调用 wx:get_env/0wx:set_env/1 来获取当前的活动环境变量,或设置给新进程使用。两个进程都各自调用 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 方式来接收处理指定的事件,这是最方便的事件处理方式。

示例程序结构要点:

  • 导出入口函数 -export([start/0]).

  • 导入 wx 头文件 -include_lib("wx/include/wx.hrl").

  • 入口函数执行时,执行初始化;

    • 执行 wx:new() 函数启动 wx 服务,初始化环境和内存映射,可以传入参数格式 {debug, Level};
    • 执行 wx:batch() 以高效批量处理 wx 的各种命令,没有它就不会处理 wxWidgets 线程的事件;
    • 执行 create_window 创建窗体,并设置状态栏 createStatusBar 再返回 Frame;
    • 执行 wxFrame:connect() 连接各种事件处理,有标题栏的 close_window、 按钮事件 command_menu_selected;
  • loop 函数中进入 receive 接收事件进行处理;

    • 注意 #wx{event=#wxClose{}} 这里的 wx 记录体在 event 嵌套了 wxClose 记录体,对应了窗口的关闭事件。
    • Msg 等待处理一个消息事件,并继续执行 loop 循环。

事件元组格式如下,有 wxCommand 按钮事件,有 窗体标题栏的关闭按钮事件,根据需要进行匹配:

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

可以给窗体设置图标 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 对象服务器,它会自动在新进程中执行模块的 init 方法,Mod:init(Args) 并返回一个窗体对象:

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

wx_object 不是 wxWidgets 的类,而是 wx 在内存里的具体物理实现,可以看做是 Erlang 中 gen_server 的 behaviour。

当然,现在是用 Erlang,还是要用它的说法,用户程序模块应该导出以下函数:

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

这样的模块定义,实现这些函数,就完成了 wx 对象服务器的结构定义。事件发生时,就会执行相应的模块方法。

示例代码如下,有改动:

%%%-------------------------------------------------------------------
%%% 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 是日语的数独,最简单的数独就是九宫格。标准 Sudoku 从整体上看,是 9 X 9 的盘格,每 3 X 3 的盘格作为一区。

Sudoku 的游戏规则非常简单,全盘的每一行、每一列,必须填进 9 个数字。每行每列的数字,必须完全不同,不允许出现重复数字,每个小区 3 X 3 的盘格也不允许出现重复数字。

Sudoku 这个程序也是基于 Erlang/OTP 工程的基本框架,即 Supervision Tree 监督树架构实现的,大量使用了 Erlang/OTP 四大 Behaviour 中的 gen_server。除 Supervisor 外,它们都在监督树充当 Worker 角色:

  • gen_server Generic server behaviour,实现 C/S 架构中的服务端;
  • gen_statem Generic state machine behaviour,实现一个有限状态机 FSM - Finite State Machine;
  • gen_event Generic event handling behavior,实现事件处理功能;
  • supervisor Generic supervisor behavior,实现监督者,它以监督树的方式存在;

Sudoku 模块分解:

  • sudoku.hrl

    引入 wx 头文件,定义结构体和菜单常量,还有一个宏 TC 包装参数方便使用主模块中的 tc 方法;

  • sudoku.erl

    是程序主进程模块,定义两个入口 gostart,还有两个内部方法:

    • init 初始化,执行 sudouku_gui:new 设置界面,同时执行 sudoku_game:init(GFX) 初始化游戏逻辑模块,并进入消息循环。GFX 是整个游戏的 GUI 界面模块,即 sudoku_gui 模块,它通过消息发送给主模块,Game ! {gfx, self()}
    • tc 封装 timer:tc 函数,即 Time counter 计时器,用来测量运行时间;
  • sudoku_gui.erl GUI 界面模块,只供 sudoku.erl 调用;

    在唯一的对外接口 new 函数中执行 wx_object:start_link 开始一个类似 gen_server behaviour 的服务器,配置主界面及事件处理,使用 wxBoxSizer 做布局,将主界面分成 TopMain 两块。Top 部分放按钮,Main 用来放棋盘,通过执行 sudoku_board:new 来初始化棋盘。在 handle_info 函数中接收来自棋盘消息,如 set_val 设置格子数值消息,并通过 validate 消息通知主模块。

  • sudoku_board.erl 是游戏棋盘实现:

    属于 GUI 功能的一部分,棋盘显示是通过 wxDC 绘图实现的,每个格子只占 Canvas 中的一个绘图区域。棋盘模块也是标准 gen_server,绑定的各种事件,如键盘事件,按数字键就可以在格子上填数字,在 handle_eventhandle_sync_event 回调函数中处理各种事件,如窗口大小变化事件 #wxSize 中就执行 redraw 重绘格子,draw_board 函数就是真正画格子方法。在鼠标移动过程中或键盘事件中,通过鼠标位置计算是哪个格子,然后再向 sudoku_gui 发送 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 是数独游戏逻辑模块,由 sudoku 模块执行初始化,init(GFX) 函数传入的 GUI 界面模块用于消息传递,整个程序的消息循环就是这个核心模块。注意,它和入口模块是同进程的,并不像 sudoku_guisudoku_board 是 wx_object behaviour,会创建新进程运行,它们和主进程的通信走消息管道。所以进入消息循环后,主要的逻辑就是主线程与 GUI 线程的消息处理,以及根据游戏规则响应。列如,validate 消息是在给格子设置数字时产生的,核心模块需要对数字进行判断,如果是相同的数字就忽略继续消息循环,否则进入 validate 函数验证,再将验证结果发送消息给 GUI 模块进行相应的绘图。

GUI 界面在关闭时,发送了一条消息 G ! quit 并给 wx_object 返回一个 {stop, shutdown, S},这个返回值不符合 wx_object 的要求,会触发异常导致不能正常关闭程序,应该在关闭时返回 {stop, Reason, State}

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

同时 G ! quit 消息在主模块消息循环中会转换为一个 halt 原子类型,主模块的入口设置的 case 条件不匹配又会导致异常:

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 代码,需要作相应修改。

draw_board 格子绘制的相关代码:

-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 绘制大、小九宫格的分割线,只是粗细的区别;
  • wxDC:drawText 在 draw_number 函数中绘制数字;

注意 redraw 方法中的 [draw_number(DC,F,BoxSz,Sq) || Sq <- S#state.board] 表达,它是列表推理 List Comprehensions,相当于枚举了各个数字并调用 draw_number 函数进行绘制。

Makefile 自动编译工具

代码变成可执行文件,叫做编译 compile,工程中通过要编译多个文件,整体叫做构建 build。

Make 是最常用的构建工具,诞生于 1977 年,主要用于 C 语言的项目。但是实际上 ,任何只要某个文件有变化,就要重新构建的项目,都可以用 Make 构建。

GNU 的 Make 工具可以替代手工的编译工作,通过 Makefile 脚本实现工程级别的编译工作自动化。

列如,以下一个 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 同一文件夹下,执行 erl -make,编译成功就会出现源代码对应的 .beam。

在 Windows 系统使用 Gnu make 命令,需要 ComSpec 这个环境变量指向 cmd.exe,或者设置 SHELL=cmd.exe 否则 shell 会执行失败:

process_begin: CreateProcess(NULL,gcc -c test.c, ...)failed. 
make(e=2): 系统找不到指定的文件 
make:*** [test.o] 错误2 

列如,Erlang 源代码中提供了 wxErlang 模块的示例,其编译脚本 otp_src_23.0\lib\wx\examples\demo\Makefile 是为 Linux 系统准备的,在 Windows 系统上使用需要修改一下;

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

然后执行编译,运行测试:

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

make 命令只是一个根据指定的 Shell 命令进行构建的工具,它的规则很简单:

  • Target 规定要构建哪个文件,用什么命令;
  • Dependence 它依赖哪些源文件;
  • Update 当那些文件有变动时,如何重新构建它。

构建规则都写在 Makefile 文件里面,这个文件由一系列规则 rules 构成:

<target> : <prerequisites> 
[tab]  <commands>
  • 第一行冒号前面的部分,叫做目标 Target,多目标用空格隔开,冒号后面的部分叫做前置条件 prerequisites。
  • 第二行必须由一个 tab 键起首,后面跟着命令 commands。
  • 目标是必需的,不可省略,前置条件和命令都是可选的,但是两者之中必须至少存在一个。
  • 每条规则就明确两件事:构建目标的前置条件是什么,以及如何构建。

目标通常是文件名,指明 Make 命令所要构建的对象,除了文件名,目标还可以是某个操作的名字,这称为伪目标 phony target。

在定义目标时,如果当前目录中,正好有一个文件同名,比如,目标叫做 clean,Make 执行时发现 clean 文件已经存在,而且是最新的状态,就认为没有必要重新构建了,就不会执行指定的命令。为了避免这种情况,可以明确声明 clean 是伪目标,写法如下:

.PHONY: clean
clean:
        rm *.o temp

声明 clean 是伪目标之后,make 就不会去检查是否存在一个叫做 clean 的文件,而是每次运行都执行对应的命令。像 .PHONY 这样的内置目标名还有不少,伪目标以句点开头跟大写字母,可以查看手册。

前置条件通常是一组文件名,之间用空格分隔。它指定了目标是否重新构建的判断标准: 只要有一个前置文件不存在,或者有过更新,前置文件的 last-modification 时间戳比目标的时间戳新,目标就需要重新构建。

命令 commands 表示如何更新目标文件,由一行或多行的 Shell 命令组成。它是构建目标的具体指令,它的运行结果通常就是生成目标文件。

Make 有隐含规则 implict rule,比如:

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

上面的规则中,没有定义 foo.o 目标,make 会自动使用隐含规则,选检查 foo.o 文件是不存在,然后检查目录下对应的源代码,比如 foo.c 文件就会执行 C 编译器,如果是 foo.p 文件则执行 Pascal 编译器,如此。

隐含规则和隐含变量是配套的,C compiler,对应的隐含变量就是 cc 命令,可以直接调用,(CC)、(CFLAGS)、$(CPPFLAGS) 等。

Make 的一些编程能力:

  • Make 支持命令换行,在换行符前加反斜杠 \ 转义,$$ 表示转义 $ 符号。

  • 井号 # 在 Makefile 中表示其后面的内容是注释。

  • 支持 *?[...] 通配符用来指定一组符合条件的文件名。

  • 支持匹配符,%,如 %.o: %.c 为当前目录下源码文件定义相应的目标。

  • 支持变量,如 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)
    
  • 内置变量,如$(CC) 指向当前使用的编译器,$(MAKE) 指向当前使用的 Make 工具。

  • 自动变量:

    • $@ 指代当前 Make 命令当前构建的那个目标。
    • $< 指代第一个前置条件。
    • $? 指代比目标更新的所有前置条件,之间以空格分隔。比如,规则为 t: p1 p2,其中 p2 的时间戳比 t 新,$? 就指代 p2。
    • $^ 指代所有前置条件,之间以空格分隔。
    • $* 指代匹配符 % 匹配的部分, 比如 % 匹配 f1.txt 中的 f1,$* 就表示 f1。
    • $(@D)$(@F) 分别指向 $@ 自动变量的目录名和文件名部分。
    • $(<D)$(<F) 分别指向 $< 自动变量的目录名和文件名部分。
  • 支持 if-else 条件判断结构:

      ifeq ($(CC),gcc)
          libs=$(libs_for_gcc)
      else
          libs=$(normal_libs)
      endif
    
  • 支持循环结构:

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

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

Makefile 提供了许多内置函数,可供调用。下面是几个常用的内置函数。

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…)

其它函数

函数 格式 作用
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} 转换成 beam 扩展名,执行 make 可以指定编译的目标:

make [Target]

就会将模块编译生成脚本定义目标文件。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335