GTK-server and Erlang
I just stumbled across GTK-server in the last couple of days (occasionally /prog/ is of some use), and it struck me (as it has others) that this would make a good companion to Erlang for GUI applications. Of course, this was just before discovering that the new Erlang 5.7/R13 beta release has an experimental wxWidgets binding included; but for the moment, not only is the GTK binding rather more mature, it also has a more immediately obvious architecture for use in an Erlang application. The process separation also avoids any shared-memory issues that having a direct FFI style binding with opaque pointers could be prone to.
The general structure of a full program would be
- spawn a process that launches the GTK-server executable
- in a short while, construct the UI widgets
- run the main loop as a tail-recursive call that handles events. If there are long-running activities, they should be separate processes with the usual message passing.
The following sample isn't quite like that because there is nothing calling back to it. The full implementation it would need to have non-blocking TCP, interleaved with whatever incoming messages needed to forward to the UI process. It would also benefit from some better data structure than the flat argument list here to loop/4...
-module(demo_tcp). | |
-export([main/0]). | |
% Demonstration on how to use the GTK-server with Erlang by TCP. | |
% Should work with most versions of Erlang on Linux and Windows platforms. | |
% | |
% based on the python example by Evan Hempel - http://echempel.freezope.org/mkbinding. | |
% as published at http://gtk-server.org/demo-tcp.py.txt | |
% | |
% At an Erlang prompt issue 'c(demo_tcp), demo_tcp:main().' | |
% this is not high throughput. It is not expected to need to be high throughput | |
recv(Socket) -> | |
recv(Socket, []). | |
recv(Socket, List) -> | |
case gen_tcp:recv(Socket, 1) of % get one byte at a time | |
{ok, "\n"} -> List; % read until we read a newline | |
{ok, X} -> recv(Socket, List ++ X); | |
{error, Reason} -> {error, Reason} % just pass on errors for the moment | |
end. | |
main() -> | |
spawn(fun() -> os:cmd("gtk-server tcp=localhost:50005") end), | |
receive | |
after 250 -> % wait a quarter second for server to start | |
true | |
end, | |
{ok, Socket} = gen_tcp:connect('localhost', 50005, [list, {active, false}]), % blocking | |
gen_tcp:send(Socket, "gtk_init NULL NULL"), | |
recv(Socket), | |
gen_tcp:send(Socket, "gtk_window_new 0"), | |
Win = recv(Socket), | |
gen_tcp:send(Socket, "gtk_window_set_title "++Win++" \"Erlang Demo program with TCP\""), | |
recv(Socket), | |
gen_tcp:send(Socket, "gtk_widget_set_usize "++Win++" 450 400"), | |
recv(Socket), | |
gen_tcp:send(Socket, "gtk_table_new 50 50 1"), | |
Table = recv(Socket), | |
gen_tcp:send(Socket, "gtk_container_add "++Win++" "++Table), | |
recv(Socket), | |
gen_tcp:send(Socket, "gtk_button_new_with_label Exit"), | |
Button = recv(Socket), | |
gen_tcp:send(Socket, "gtk_table_attach_defaults "++Table++" "++Button++" 41 49 45 49"), | |
recv(Socket), | |
gen_tcp:send(Socket, "gtk_entry_new"), | |
Entry = recv(Socket), | |
gen_tcp:send(Socket, "gtk_table_attach_defaults "++Table++" "++Entry++" 1 40 45 49"), | |
recv(Socket), | |
gen_tcp:send(Socket, "gtk_text_new NULL NULL"), | |
Text = recv(Socket), | |
gen_tcp:send(Socket, "gtk_table_attach_defaults "++Table++" "++Text++" 1 49 8 44"), | |
recv(Socket), | |
gen_tcp:send(Socket, "gtk_radio_button_new_with_label_from_widget NULL Yes"), | |
Radio1 = recv(Socket), | |
gen_tcp:send(Socket, "gtk_table_attach_defaults "++Table++" "++Radio1++" 1 10 1 4"), | |
recv(Socket), | |
gen_tcp:send(Socket, "gtk_radio_button_new_with_label_from_widget "++Radio1++" No"), | |
Radio2 = recv(Socket), | |
gen_tcp:send(Socket, "gtk_table_attach_defaults "++Table++" "++Radio2++" 1 10 4 7"), | |
recv(Socket), | |
gen_tcp:send(Socket, "gtk_widget_show_all "++Win), | |
recv(Socket), | |
gen_tcp:send(Socket, "gtk_widget_grab_focus "++Entry), | |
recv(Socket), | |
loop(Socket, Entry, Button, Text). | |
loop(Socket, Entry, Button, Text) -> | |
gen_tcp:send(Socket, "gtk_server_callback WAIT"), | |
case recv(Socket) of | |
Entry -> | |
gen_tcp:send(Socket, "gtk_entry_get_text "++Entry), | |
Tmp = recv(Socket), | |
gen_tcp:send(Socket, "gtk_text_insert "++Text++" NULL NULL NULL "++Tmp++" -1"), | |
recv(Socket), | |
gen_tcp:send(Socket, "gtk_entry_set_text "++Entry++" \" \""), | |
recv(Socket), | |
loop(Socket, Entry, Button, Text); | |
Button -> | |
gen_tcp:send(Socket, "gtk_server_exit"); % exit | |
{_, Reason} -> | |
io:format( "Error ~s ~n", [Reason]), | |
gen_tcp:send(Socket, "gtk_server_exit"); % exit | |
_ -> | |
loop(Socket, Entry, Button, Text) | |
end. |
A finished application would also need some real error handling, but that's possibly overkill for a proof-of-concept.
You can use fifos instead quite happily. On Windows, it looks like this:
recv(In) -> | |
recv(In, []). | |
recv(Socket, List) -> % can't just use io:get_line/2 since that tries to write a prompt to a read-only device | |
case file:read(Socket, 1) of % get one byte at a time | |
{ok, "\n"} -> List; % read until we read a newline | |
{ok, X} -> recv(Socket, List ++ X); | |
{error, Reason} -> {error, Reason} % just pass on errors for the moment | |
end. | |
main() -> | |
spawn(fun() -> os:cmd("gtk-server fifo") end), | |
receive | |
after 250 -> % wait a quarter second for server to start | |
true | |
end, | |
{ok, Out} = file:open("\\\\.\\pipe\\out", [write]), | |
{ok, In} = file:open("\\\\.\\pipe\\in", [read]), | |
file:write(Out, "gtk_init NULL NULL"), | |
recv(In), |
with the rest of the changes being systematic.
No comments :
Post a Comment