Programming C, C++, Java, PHP, Ruby, Turing, VB
Computer Science Canada 
Programming C, C++, Java, PHP, Ruby, Turing, VB  

Username:   Password: 
 RegisterRegister   
 GUIs Don't Have to be Hard, Part 1: Gooey O'Caml
Index -> Programming, General Programming -> Functional Programming
View previous topic Printable versionDownload TopicSubscribe to this topicPrivate MessagesRefresh page View next topic
Author Message
wtd




PostPosted: Thu Jun 02, 2005 7:05 pm   Post subject: GUIs Don't Have to be Hard, Part 1: Gooey O'Caml

Introduction

For many young programmers, new to the field, a special interest lies in creating graphical applications. Aside from any other reasons, perhaps this is because GUIs are seen as somehow "magical" and beyond the reach of mere mortals.

The flip side of this is that a lot of young programmers are afraid to start programming. or to continue past command-line applications.

To dispell some of the aura that surrounds GUI programming, I'll start out with some simple applications in one of my favorite languages, O'Caml, and using the GTK toolkit on Ubuntu Linux. Please ask for information on setting up the necessary tools.

First, though...

A look at the console, and the kind of thing we're leaving behind (for the most part). Let's say "Hello world".

code:
print_endline "Hello world";;


Pretty simple, isn't it? Almost feels like we haven't really done any programming at all. Well, we have. Our first GUI programs won't do much more either.

And now for graphical goodness...

Posted Image, might have been reduced in size. Click Image to view fullscreen.

code:
open GWindow
open GMain;;

let w = window
   ~title:"Hello world"
   ~height:300
   ~width:300
   ();;

w#show ();;
w#connect#destroy ~callback:Main.quit;;
Main.main ()


Open

The "open" lines at the beginning of the program are simply syntactic conveniences. With them we can avoid typing, for instance:

code:
GWindow.window


Instead we simply write:

code:
window


The "window" function

This is a helper function which takes several labelled arguments and returns a new window. In this case we tell it the title of the window, as well as its height and width.

Once we've created a window we use a "let" binding to give the window a name.

Making the window show up

The window is an object, so we call a method on it to make it show up. Whereas must languages use "." for calling methods, O'Caml uses "#".

Connecting an event

The simplest event that affects almost every window is being destroyed. We trigger this by clicking the close button. We need to provide a function that the program runs when that happens. In this case it's the "quit" function in the "Main" module (from "GMain").

Putting it all in motion

In every GUI app there's a loop which runs continuously, waiting for things to happen. In this case, it takes the form of the "main" function in the "GMain.Main" module.

Compiling it

I called my source file "helloworld.ml", so I used to the following command to compile this program.

code:
ocamlc -I +lablgtk2 -o helloworld lablgtk.cma gtkInit.cmo helloworld.ml


More information on what's going on here can be made available upon request.

A little more

Let's put some text into the center of the screen.

Posted Image, might have been reduced in size. Click Image to view fullscreen.

code:
open GWindow
open GMain
open GMisc;;

let w = window
   ~title:"Label Test"
   ~height:300
   ~width:300
   ()

let l = label
   ~text:"Hello world!"
   ~packing:w#add
   ();;

w#show ();;
w#connect#destroy ~callback:Main.quit;;
Main.main ()


What's new?

In this new program I've got most everything the way it was, but there are a few differences. I've create another object. This time it's a label with the text "Hello world!".

The interesting part is the packing argument. With this I provide a function which, when called, adds the object to whatever container it's supposed to belong to. In this case I simply gave it the window's "add" method.

Note: windows can only contain a single object. Yes, there are ways around this. Smile

More than text... a control

We've added a simple text label to our window, but that doesn't do much. Instead, let's add a button that simply prints "Hello world" to the console.

Posted Image, might have been reduced in size. Click Image to view fullscreen.

code:
open GWindow
open GMain
open GButton;;

let w = window
   ~title:"Button Test"
   ~height:300
   ~width:300
   ~border_width:30
   ()

let b = button
   ~label:"Say Hello"
   ~packing:w#add
   ();;

w#show ();;
w#connect#destroy ~callback:Main.quit;;
b#connect#clicked ~callback:(fun _ -> print_endline "Hello world");
Main.main ()


Creating the button widget

code:
let b = button
   ~label:"Say Hello"
   ~packing:w#add
   ()


There isn't much new here. The "label" argument is new, but self-explanatory. It's just the text that appears inside the button. Again, we pack the button by adding it to the window as it's sole contents.

Making the button do something

code:
b#connect#clicked ~callback:(fun _ -> print_endline "Hello world")


This looks a lot like the code we used to quit the application when the window is destroyed. It's just connecting an event with an action. We don't have a function, though, that will print "Hello world" to the screen.

Fortunately, O'Caml gives us the ability to define anonymous functions.

code:
fun _ -> print_endline "Hello world"


Here we create a simple anonymous function which takes a single argument which we don't care about (as denoted by the underscore), and then prints "Hello world" to the console.

Again, one widget at a time isn't useful

So let's look at a simple app with more than one widget. With this app we'll have a text entry field, a label for it, and a button, that when clicked prints the contents of the entry field to the console.

Oh yes, and nothing should happen if there's nothing in the entry field. If some text in the field is selected, only that text should be displayed.

Posted Image, might have been reduced in size. Click Image to view fullscreen.

code:
open GWindow
open GMain
open GButton
open GMisc
open GEdit
open GPack
open Gtk.Tags;;

let w = window
   ~title:"Hbox Test"
   ~height:300
   ~width:300
   ~allow_shrink:true
   ()

let vb = vbox
   ~spacing:4
   ~border_width:5
   ~packing:w#add   
   ()

let hb_top = hbox
   ~spacing:4
   ~packing:vb#pack
   ()
   
let hb_bottom = hbox
   ~spacing:4
   ~packing:(vb#pack ~from:`END)
   ()

let sep = separator `HORIZONTAL
   ~packing:(vb#pack ~from:`END)
   ()

let l = label
   ~text:"Name:"
   ~packing:(hb_top#pack ~expand:false)
   ()

let e = entry
   ~packing:(hb_top#pack ~expand:true)
   ()

let b = button
   ~label:"Greet"
   ~packing:(hb_bottom#pack ~from:`END ~expand:false ~fill:false)
   ()

let print_name () =
   if e#text <> "" then
      match e#selection with
           None -> print_endline e#text
         | Some (first, last) ->
             let s = e#get_chars first last in
                print_endline s;;       

w#show ();;
w#connect#destroy ~callback:Main.quit;;
b#connect#clicked ~callback:print_name;;
Main.main ()


Lots of opening going on

code:
open GWindow
open GMain
open GButton
open GMisc
open GEdit
open GPack
open Gtk.Tags


In order, we have modules for windows, the main event loop that drives an application, buttons, miscellaneous controls (including labels), editable fields, like our entry field, containers (which we'll use to add multiple widgets to a window, and tags used to modify widgets.

First, the window

As usual, first I create a window, and the only thing that's new this time around is the "allow_shrink" argument. By setting this to true, I can resize the window to be smaller.

Next a box or three

code:
let vb = vbox
   ~spacing:4
   ~border_width:5
   ~packing:w#add   
   ()


Here I create a vertical box with a spacing of 4 pixels between its contents and a 5 pixel border. I add it to the window.

code:
let hb_top = hbox
   ~spacing:4
   ~packing:vb#pack
   ()


code:
let hb_bottom = hbox
   ~spacing:4
   ~packing:(vb#pack ~from:`END)
   ()


Into this vertical box I add two horizontally oriented boxes. Each of these also has a spacing of 4 pixels. I pack the top box into the vertical box normally, but the bottom box I specify should be packed from the end of the vertical box, rather than the start, which is the default.

A separator

code:
let sep = separator `HORIZONTAL
   ~packing:(vb#pack ~from:`END)
   ()


To separate the two boxes, I create a simple horizontal separator and pack it into the vertical box from the end as well.

The actual controls

code:
let l = label
   ~text:"Name:"
   ~packing:(hb_top#pack ~expand:false)
   ()

let e = entry
   ~packing:(hb_top#pack ~expand:true)
   ()

let b = button
   ~label:"Greet"
   ~packing:(hb_bottom#pack ~from:`END ~expand:false ~fill:false)
   ()


For all of the work we did setting up the layout, we have but three controls. The label we've seen before. We simply change how it's packed. Instead of adding it to the window, we pack it into the top horizontal box. Oh, and we tell it not to expand. Even if we increase the size of the window, it will not increase the size of the label.

The entry field is new, but not terribly surprising. It is allowed to expand, however.

The button's not new. We do tell it not to expand, and we pack it from the end, so it appears right-justified.

Making the button do something

The window code is familiar, so we'll skip over that.

Instead let's look at the code that makes the button actually do something.

code:
b#connect#clicked ~callback:print_name


Ok but to understand this we need to look at the print_name function.

code:
let print_name () =
   if e#text <> "" then
      match e#selection with
           None -> print_endline e#text
         | Some (first, last) ->
             let s = e#get_chars first last in
                print_endline s


Here we use the "text" method of the entry field to get its contents as a string. If the field is not an empty string, then we can proceed.

Next we use the "selection" method. This either returns None or some set of start and end positions for the text selected. If it's None, then we print the entire string. Otherwise we use the "get_chars" method to retrieve the selected text and print that.

A minor modification

Up until now I've explicitly stated the size of the new window, but as we can see, clearly that was too large. What if we could get a window that's just the right size?

By commenting out a few lines in the code that creates the window we can do just that.

code:
let w = window
   ~title:"Hbox Test"
   (* ~height:300
   ~width:300
   ~allow_shrink:true *)
   ()


Posted Image, might have been reduced in size. Click Image to view fullscreen.

And now for some tables

No, I'd like to take this last example a bit farther, and have it prompt for a first and last name. Now, these two fields should line up, so to keep everything lined up properly, we're going to have to use a table. If we simply used two horizontal boxes, they might not line up correctly if the labels were of different widths.

Posted Image, might have been reduced in size. Click Image to view fullscreen.

code:
open GWindow
open GMain
open GButton
open GMisc
open GEdit
open GPack
open Gtk.Tags;;

let w = window ~title:"Table Test" ()

let vb = vbox
   ~spacing:4
   ~border_width:5
   ~packing:w#add   
   ()

let t = table
  ~rows:2
  ~columns:2
  ~packing:vb#pack
  ~col_spacings:4
  ~row_spacings:4
  ()
 
let hb_bottom = hbox
   ~spacing:4
   ~packing:(vb#pack ~from:`END)
   ()

let sep = separator `HORIZONTAL ~packing:(vb#pack ~from:`END) ()

let ls = [|
   label
      ~text:"First Name:"
      ~packing:(t#attach ~top:0 ~left:0 ~expand:`NONE)
      () ;
   label
      ~text:"Last Name:"
      ~packing:(t#attach ~top:1 ~left:0 ~expand:`NONE)
      () |]

let es = [|
   entry
      ~packing:(t#attach ~top:0 ~left:1 ~expand:`X)
      () ;
   entry
      ~packing:(t#attach ~top:1 ~left:1 ~expand:`X)
      () |]

let b = button
   ~label:"Greet"
   ~packing:(hb_bottom#pack ~from:`END ~expand:false ~fill:false)
   ()

let print_name () =
   let fname = es.(0)#text and
       lname = es.(1)#text
   in
      match (fname, lname) with
           ("", "") -> ()
         | ("", n) -> print_endline ("Hello, " ^ n)
         | (n, "") -> print_endline ("Hello, " ^ n)
         | (m, n) -> print_endline ("Hello, " ^ m ^ " " ^ n);;

w#show ();;
w#connect#destroy ~callback:Main.quit;;
b#connect#clicked ~callback:print_name;;
Main.main ()


Not a lot of new stuff

All told, much of this program is copied and pasted from the previous version.

Enter tables

code:
let t = table
  ~rows:2
  ~columns:2
  ~packing:vb#pack
  ~col_spacings:4
  ~row_spacings:4
  ()


Creation of the table is fairly self-explanatory.

Enter arrays

Rather than have two separate variables for the labels and entry fields, I used arrays. As a simpler example, an array of three numbers:

code:
[| 1; 2; 3 |]


O'Caml features terrific support for lists as well, but arrays are faster, and not any more difficult to work with, really.

Packing code

code:
t#attach ~top:0 ~left:1 ~expand:`X


You will notice that the packing code looks a bit different. When we put a widget into a table we say we're attaching it to that table, and we provide information on where in the table to attach it.

As with arrays, we start counting at zero, so the above describes a widget placed in the first row, and second column.

Printing the name

code:
let print_name () =
   let fname = es.(0)#text and
       lname = es.(1)#text
   in
      match (fname, lname) with
           ("", "") -> ()
         | ("", n) -> print_endline ("Hello, " ^ n)
         | (n, "") -> print_endline ("Hello, " ^ n)
         | (m, n) -> print_endline ("Hello, " ^ m ^ " " ^ n)


This is one area where we've really changed things, so let's look at it in detail.

code:
   let fname = es.(0)#text and
       lname = es.(1)#text
   in


Here I'm creating a couple of local bindings for the text contents of the entry fields. These names are a bit more convenient.

code:
es.(0)


This is an example of accessing a specific element in an array.

code:
match (fname, lname) with


Here I've created a tuple containing the first and last name values, and I'm telling O'Caml I want to pattern match against those values.

code:
("", "") -> ()


The first pattern is two empty strings. In this case I do nothing.

code:
("", n) -> print_endline ("Hello, " ^ n)


Here I have the pattern where the first name is empty, but the last name is not. If it were, it would have been matched by the first pattern.

In this case I simply greet by the last name.

code:
(n, "") -> print_endline ("Hello, " ^ n)


This pattern says the first name is not empty (or it would have been matched previously), and the last name is empty.

In this case, I want to only greet the first name.

code:
(m, n) -> print_endline ("Hello, " ^ m ^ " " ^ n)


This is the default pattern. If nothing else matches, this will. With the other patterns I have in place, I know this means that neither is empty.

In this case I greet both.

Note: ^ is the O'Caml string concatenation operator.

Shedding the console

So far our output has been to the console. Instead, let's display our output in a dialog window. Only the "print_name" function's code changes, so I won't be posting the rest of the code.

Posted Image, might have been reduced in size. Click Image to view fullscreen.

code:
let print_name () =
   let fname = es.(0)#text and
       lname = es.(1)#text and
       make_dialog ~name =
          let d = message_dialog
             ~message:("Hello, " ^ name)
             ~message_type:`INFO
             ~title:"Hello!"
             ~buttons:Buttons.close
             ()
          in
             d#connect#response ~callback:(fun _ -> d#destroy ());
             d in
   let show_dialog ~name =
          let d = make_dialog ~name in
             d#show ()
   in
      match (fname, lname) with
           ("", "") -> ()
         | ("", n) -> show_dialog n
         | (n, "") -> show_dialog n
         | (m, n) -> show_dialog (m ^ " " ^ n)


On local functions

The print_name function now makes extensive use of local functions. The first is a function called "make_dialog" which takes the name to greet and generates an appropriate dialog window, automating much of the tedium involved, including handling events. The second simply takes the dialog generated by the first, and makes it show up on the screen.

Making a dialog

code:
make_dialog ~name =
   let d = message_dialog
      ~message:("Hello, " ^ name)
      ~message_type:`INFO
      ~title:"Hello!"
      ~buttons:Buttons.close
      ()
   in
      d#connect#response ~callback:(fun _ -> d#destroy ());
      d


This fairly straightforward. I use the message_dialog function to create the dialog window which is bound to "d", locally. With the arguments I supply the message to be displayed, the type of message (in this case, info), the title of the dialog window, and the buttons to display.

The function then takes that dialog window and adds an event handler. In this case a function which takes an argument about which we don't care, and then destroys the dialog window.

The function then returns the dialog window, so some other function can manipulate it.

Showing the dialog

code:
let show_dialog ~name =
   let d = make_dialog ~name in
      d#show ()


The "show_dialog" function is very simple. It simply makes a dialog, then calls that dialog's show method.

When it all comes together

code:
match (fname, lname) with
     ("", "") -> ()
   | ("", n) -> show_dialog n
   | (n, "") -> show_dialog n
   | (m, n) -> show_dialog (m ^ " " ^ n)


Now for each pattern I need only call the very simple show_dialog function and all of the details are taken care of for me.

Organizing widgets a bit more

Going back to some cosmetic stuff, without changing how the app really works, let's look at grouping those entry fields a bit. Both the entry fields are for names, so having "Name" in both labels seems redundant. Let's create a labelled frame to hold both fields.

Posted Image, might have been reduced in size. Click Image to view fullscreen.

code:
open GWindow
open GMain
open GButton
open GMisc
open GEdit
open GPack
open GBin
open Gtk.Tags;;

let w = window ~title:"Dialog Test" ()

let vb = vbox
   ~spacing:4
   ~border_width:5
   ~packing:w#add   
   ()

let f = frame
   ~label:"Name"
   ~label_xalign:0.5
   ~packing:vb#pack
   ~shadow_type:`OUT
   ()

let t = table
   ~rows:2
   ~columns:2
   ~packing:f#add
   ~col_spacings:4
   ~row_spacings:4
   ~border_width:5
   ()
 
let hb_bottom = hbox
   ~spacing:4
   ~packing:(vb#pack ~from:`END)
   ()

let ls = [|
   label
      ~text:"First:"
      ~packing:(t#attach ~top:0 ~left:0 ~expand:`NONE)
      () ;
   label
      ~text:"Last:"
      ~packing:(t#attach ~top:1 ~left:0 ~expand:`NONE)
      () |]

let es = [|
   entry
      ~packing:(t#attach ~top:0 ~left:1 ~expand:`X)
      () ;
   entry
      ~packing:(t#attach ~top:1 ~left:1 ~expand:`X)
      () |]

let b = button
   ~label:"Greet"
   ~packing:(hb_bottom#pack ~from:`END ~expand:false ~fill:false)
   ()

let print_name () =
   let fname = es.(0)#text and
       lname = es.(1)#text and
       make_dialog ~name =
          let d = message_dialog
             ~message:("Hello, " ^ name)
             ~message_type:`INFO
             ~title:"Hello!"
             ~buttons:Buttons.close
             ()
          in
             d#connect#response ~callback:(fun _ -> d#destroy ());
             d in
   let show_dialog ~name =
          let d = make_dialog ~name in
             d#show ()
   in
      match (fname, lname) with
           ("", "") -> ()
         | ("", n) -> show_dialog n
         | (n, "") -> show_dialog n
         | (m, n) -> show_dialog (m ^ " " ^ n);;

w#show ();;
w#connect#destroy ~callback:Main.quit;;
b#connect#clicked ~callback:print_name;;
Main.main ()


What's new and what's gone

Obviously, the frame is new. Additionally, the horizontal separator is gone, since the frame provides a nice visual break by itself.

A frame only contains one widget. In this case that widget is the table we'd previously created. We also add a border to the table since there's no "padding" within a frame.

Fortunately, since a frame only contains one widget, packing widgets into it is straightforward.

Building a frame

code:
let f = frame
   ~label:"Name"
   ~label_xalign:0.5
   ~packing:vb#pack
   ~shadow_type:`OUT
   ()


The label argument is pretty straightforward. The "label_xalign" argument governs how far over the label is. This is a floating point number from 0 to 1. In ths case 0.5 indicates the center.

The shadow_type argument is simply set to out here due to my own aesthetic preferences. In other words, I like the way it looks. Smile

A few new things: notebooks and saving

In the previous example, I used a frame to group the name entry widgets. Now, let's use a notebook. It's easier to show you what that means than to describe it, though here I've only got one tab, so it may look a bit odd. Don't worry. More tabs are coming.

Also new in this revision is that I've added the ability to not only greet the person who enters their name, but also the ability to save that information to a file.

Posted Image, might have been reduced in size. Click Image to view fullscreen.

code:
open GWindow
open GMain
open GButton
open GMisc
open GEdit
open GPack
open Gtk.Tags;;

let w = window ~title:"Notebook and Save Test" ()

let vb = vbox
   ~spacing:4
   ~border_width:5
   ~packing:w#add   
   ()

let n = notebook ~packing:vb#pack ()

let t = table
   ~rows:2
   ~columns:2
   ~packing:(n#append_page ~tab_label:(label ~text:"Name" ())#coerce)
   ~col_spacings:4
   ~row_spacings:4
   ~border_width:5
   ()
 
let hb_bottom = hbox
   ~spacing:4
   ~packing:(vb#pack ~from:`END)
   ()

let ls = [|
   label
      ~text:"First:"
      ~packing:(t#attach ~top:0 ~left:0 ~expand:`NONE)
      () ;
   label
      ~text:"Last:"
      ~packing:(t#attach ~top:1 ~left:0 ~expand:`NONE)
      () |]

let es = [|
   entry
      ~packing:(t#attach ~top:0 ~left:1 ~expand:`X)
      () ;
   entry
      ~packing:(t#attach ~top:1 ~left:1 ~expand:`X)
      () |]

let b_save = button
   ~label:"Save"
   ~packing:(hb_bottom#pack ~from:`END ~expand:false ~fill:false)
   ()

let b_greet = button
   ~label:"Greet"
   ~packing:(hb_bottom#pack ~from:`END ~expand:false ~fill:false)
   ()

let print_name () =
   let fname = es.(0)#text and
       lname = es.(1)#text and
       make_dialog ~name =
          let d = message_dialog
             ~message:("Hello, " ^ name)
             ~message_type:`INFO
             ~title:"Hello!"
             ~buttons:Buttons.close
             ()
          in
             d#connect#response ~callback:(fun _ -> d#destroy ());
             d in
   let show_dialog ~name =
          let d = make_dialog ~name in
             d#show ()
   in
      match (fname, lname) with
           ("", "") -> ()
         | ("", n) -> show_dialog n
         | (n, "") -> show_dialog n
         | (m, n) -> show_dialog (m ^ " " ^ n)

let save_info () =
   let file_chooser = file_chooser_dialog `SAVE () in
   let save_info_to_file filename =
          let fname = es.(0)#text
          and lname = es.(1)#text
          and fo = open_out filename in
             output_string fo (fname ^ "\n");
             output_string fo (lname ^ "\n");
             flush fo;
             close_out fo
   in
      file_chooser#add_button_stock `CANCEL `CANCEL;
      file_chooser#add_select_button_stock `SAVE `ACCEPT;
      if file_chooser#run () = `ACCEPT then
         match file_chooser#filename with
              None -> ()
            | Some fn -> save_info_to_file fn
      else
         file_chooser#destroy ();
      file_chooser#destroy ();
      ();;

w#show ();;
w#connect#destroy ~callback:Main.quit;;
b_greet#connect#clicked ~callback:print_name;;
b_save#connect#clicked ~callback:save_info;;
Main.main ()


What's new?

code:
let n = notebook ~packing:vb#pack ()

let t = table
   ~rows:2
   ~columns:2
   ~packing:(n#append_page ~tab_label:(label ~text:"Name" ())#coerce)
   ~col_spacings:4
   ~row_spacings:4
   ~border_width:5
   ()


The creation of the notebook and packing a widget (in this case the table) into it is pretty straightforward.

Also, we've added a "Save" button.

More important, though...

code:
let save_info () =
   let file_chooser = file_chooser_dialog `SAVE () in
   let save_info_to_file filename =
          let fname = es.(0)#text
          and lname = es.(1)#text
          and fo = open_out filename in
             output_string fo (fname ^ "\n");
             output_string fo (lname ^ "\n");
             flush fo;
             close_out fo
   in
      file_chooser#add_button_stock `CANCEL `CANCEL;
      file_chooser#add_select_button_stock `SAVE `ACCEPT;
      if file_chooser#run () = `ACCEPT then
         match file_chooser#filename with
              None -> ()
            | Some fn -> save_info_to_file fn
      else
         file_chooser#destroy ();
      file_chooser#destroy ();
      ()


The big addition is the code to save the contents of the two entry fields to a file, and to open a file chooser dialog window.

code:
let file_chooser = file_chooser_dialog `SAVE ()


Creating the file chooser window is simple. We simply have to provide it with a mode. In this case we're saving. Such a mode means we're given the option to specify a new filename.

code:
file_chooser#add_button_stock `CANCEL `CANCEL;
file_chooser#add_select_button_stock `SAVE `ACCEPT


We then add the desired buttons to the window.

code:
if file_chooser#run () = `ACCEPT then
   match file_chooser#filename with
        None -> ()
      | Some fn -> save_info_to_file fn
else
   file_chooser#destroy ()


We then run the dialog, and if the result is that we clicked the save button, check to see the filename that was selected. If no filename was selected, do nothing. Otherwise call the "save_info_to_file" function with the filename.

If the cancel button was clicked, simply destroy the dialog window.

code:
file_chooser#destroy ()


We clean up by making absolutely sure the dialog gets destroyed.

Actually saving the information to the file

code:
let save_info_to_file filename =
   let fname = es.(0)#text
   and lname = es.(1)#text
   and fo = open_out filename
   in
      output_string fo (fname ^ "\n");
      output_string fo (lname ^ "\n");
      flush fo;
      close_out fo


This is where the action is really taking place. We take the filename given to the function, open that file, and write each name to it on separate lines. We then flush the channel to make sure everything is written and then close the file.

Getting information back from a file

The previous example featured the ability to save data to a file. The only problem is that it showed no way to get that information back. This revision will do just that.

Posted Image, might have been reduced in size. Click Image to view fullscreen.

code:
open GWindow
open GMain
open GButton
open GMisc
open GEdit
open GPack
open Gtk.Tags;;

let w = window ~title:"Notebook and Save/Load Test" ()

let vb = vbox
   ~spacing:4
   ~border_width:5
   ~packing:w#add   
   ()

let n = notebook ~packing:vb#pack ()

let t = table
   ~rows:2
   ~columns:2
   ~packing:(n#append_page ~tab_label:(label ~text:"Name" ())#coerce)
   ~col_spacings:4
   ~row_spacings:4
   ~border_width:5
   ()
 
let hb_bottom = hbox
   ~spacing:4
   ~packing:(vb#pack ~from:`END)
   ()

let ls = [|
   label
      ~text:"First:"
      ~packing:(t#attach ~top:0 ~left:0 ~expand:`NONE)
      () ;
   label
      ~text:"Last:"
      ~packing:(t#attach ~top:1 ~left:0 ~expand:`NONE)
      () |]

let es = [|
   entry
      ~packing:(t#attach ~top:0 ~left:1 ~expand:`X)
      () ;
   entry
      ~packing:(t#attach ~top:1 ~left:1 ~expand:`X)
      () |]

let b_load = button
   ~label:"Load"
   ~packing:(hb_bottom#pack ~from:`END ~expand:false ~fill:false)
   ()

let b_save = button
   ~label:"Save"
   ~packing:(hb_bottom#pack ~from:`END ~expand:false ~fill:false)
   ()

let b_greet = button
   ~label:"Greet"
   ~packing:(hb_bottom#pack ~from:`END ~expand:false ~fill:false)
   ()

let print_name () =
   let fname = es.(0)#text and
       lname = es.(1)#text and
       make_dialog ~name =
          let d = message_dialog
             ~message:("Hello, " ^ name)
             ~message_type:`INFO
             ~title:"Hello!"
             ~buttons:Buttons.close
             ()
          in
             d#connect#response ~callback:(fun _ -> d#destroy ());
             d in
   let show_dialog ~name =
          let d = make_dialog ~name in
             d#show ()
   in
      match (fname, lname) with
           ("", "") -> ()
         | ("", n) -> show_dialog n
         | (n, "") -> show_dialog n
         | (m, n) -> show_dialog (m ^ " " ^ n)

let save_info () =
   let file_chooser = file_chooser_dialog `SAVE () in
   let save_info_to_file filename =
          let fname = es.(0)#text
          and lname = es.(1)#text
          and fo = open_out filename in
             output_string fo (fname ^ "\n");
             output_string fo (lname ^ "\n");
             flush fo;
             close_out fo
   in
      file_chooser#add_button_stock `CANCEL `CANCEL;
      file_chooser#add_select_button_stock `SAVE `ACCEPT;
      if file_chooser#run () = `ACCEPT then
         match file_chooser#filename with
              None -> ()
            | Some fn -> save_info_to_file fn
      else
         file_chooser#destroy ();
      file_chooser#destroy ();
      ()

let load_info () =
   let file_chooser = file_chooser_dialog `OPEN () in
   let load_info_from_file filename =
          let fi = open_in filename in
             es.(0)#set_text (try input_line fi with End_of_file -> "");
             es.(1)#set_text (try input_line fi with End_of_file -> "");
             close_in fi
   in
      file_chooser#add_button_stock `CANCEL `CANCEL;
      file_chooser#add_select_button_stock `OPEN `ACCEPT;
      if file_chooser#run () = `ACCEPT then
         match file_chooser#filename with
              None -> ()
            | Some fn -> load_info_from_file fn
      else
         file_chooser#destroy ();
      file_chooser#destroy ();
      ();;

w#show ();;
w#connect#destroy ~callback:Main.quit;;
b_greet#connect#clicked ~callback:print_name;;
b_save#connect#clicked ~callback:save_info;;
b_load#connect#clicked ~callback:load_info;;
Main.main ()


Not much new here

The "load_info" function is nearly identical to the "save-Info" function, except that it reads from a file, instead of writing to one.

Exception handling with "try ... with ..." is provided in case the file is too short and doesn't exactly conform to what we expect from a data file here.

A slight refactoring for greater flexibility

The use of local functions in the previous example was clever, but a poor choice ultimately. The ability to load data from a file and save data to one should not be tied to the action of opening a file chooser. By placing these functions in the top level of the program, they become available in other ways.

The next few revisions will show why that's important.

code:
let save_info_to_file filename =
   let fname = es.(0)#text
   and lname = es.(1)#text
   and fo = open_out filename in
      output_string fo (fname ^ "\n");
      output_string fo (lname ^ "\n");
      flush fo;
      close_out fo

let save_info () =
   let file_chooser = file_chooser_dialog `SAVE () in
      file_chooser#add_button_stock `CANCEL `CANCEL;
      file_chooser#add_select_button_stock `SAVE `ACCEPT;
      if file_chooser#run () = `ACCEPT then
         match file_chooser#filename with
              None -> ()
            | Some fn -> save_info_to_file fn
      else
         file_chooser#destroy ();
      file_chooser#destroy ();
      ()

let load_info_from_file filename =
   let fi = open_in filename in
      es.(0)#set_text (try input_line fi with End_of_file -> "");
      es.(1)#set_text (try input_line fi with End_of_file -> "");
      close_in fi

let load_info () =
   let file_chooser = file_chooser_dialog `OPEN () in
      file_chooser#add_button_stock `CANCEL `CANCEL;
      file_chooser#add_select_button_stock `OPEN `ACCEPT;
      if file_chooser#run () = `ACCEPT then
         match file_chooser#filename with
              None -> ()
            | Some fn -> load_info_from_file fn
      else
         file_chooser#destroy ();
      file_chooser#destroy ();
      ()


Menus

Unfortunately our last demo suffers from a serious problem. We just have too many buttons on the bottom of the window. The save and load functions are the kind we'd expect to see in a menu, so let's put them there.

Posted Image, might have been reduced in size. Click Image to view fullscreen.

code:
open GWindow
open GMain
open GButton
open GMisc
open GEdit
open GPack
open GMenu
open Gtk.Tags;;

let w = window ~title:"Menu Test" ()

let master_vbox = vbox ~packing:w#add ()

let m = menu_bar ~packing:master_vbox#pack ()

let file_menu = menu ()

let file_item = menu_item
   ~label:"File"
   ~packing:m#append
   ()

let load_item = image_menu_item
   ~stock:`OPEN
   ~packing:file_menu#append
   ()

let save_item = image_menu_item
   ~stock:`SAVE
   ~packing:file_menu#append
   ()

let sep_item = separator_item ~packing:file_menu#append ()

let quit_item = image_menu_item
   ~stock:`QUIT
   ~packing:file_menu#append
   ()

let vb = vbox
   ~spacing:4
   ~border_width:5
   ~packing:master_vbox#pack   
   ()

let n = notebook ~packing:vb#pack ()

let t = table
   ~rows:2
   ~columns:2
   ~packing:(n#append_page ~tab_label:(label ~text:"Name" ())#coerce)
   ~col_spacings:4
   ~row_spacings:4
   ~border_width:5
   ()
 
let hb_bottom = hbox
   ~spacing:4
   ~packing:(vb#pack ~from:`END)
   ()

let ls = [|
   label
      ~text:"First:"
      ~packing:(t#attach ~top:0 ~left:0 ~expand:`NONE)
      () ;
   label
      ~text:"Last:"
      ~packing:(t#attach ~top:1 ~left:0 ~expand:`NONE)
      () |]

let es = [|
   entry
      ~packing:(t#attach ~top:0 ~left:1 ~expand:`X)
      () ;
   entry
      ~packing:(t#attach ~top:1 ~left:1 ~expand:`X)
      () |]

let b_greet = button
   ~label:"Greet"
   ~packing:(hb_bottom#pack ~from:`END ~expand:false ~fill:false)
   ()

let print_name () =
   let fname = es.(0)#text and
       lname = es.(1)#text and
       make_dialog ~name =
          let d = message_dialog
             ~message:("Hello, " ^ name)
             ~message_type:`INFO
             ~title:"Hello!"
             ~buttons:Buttons.close
             ()
          in
             d#connect#response ~callback:(fun _ -> d#destroy ());
             d in
   let show_dialog ~name =
          let d = make_dialog ~name in
             d#show ()
   in
      match (fname, lname) with
           ("", "") -> ()
         | ("", n) -> show_dialog n
         | (n, "") -> show_dialog n
         | (m, n) -> show_dialog (m ^ " " ^ n)

let save_info_to_file filename =
   let fname = es.(0)#text
   and lname = es.(1)#text
   and fo = open_out filename in
      output_string fo (fname ^ "\n");
      output_string fo (lname ^ "\n");
      flush fo;
      close_out fo

let save_info () =
   let file_chooser = file_chooser_dialog `SAVE () in
      file_chooser#add_button_stock `CANCEL `CANCEL;
      file_chooser#add_select_button_stock `SAVE `ACCEPT;
      if file_chooser#run () = `ACCEPT then
         match file_chooser#filename with
              None -> ()
            | Some fn -> save_info_to_file fn
      else
         file_chooser#destroy ();
      file_chooser#destroy ();
      ()

let load_info_from_file filename =
   let fi = open_in filename in
      es.(0)#set_text (try input_line fi with End_of_file -> "");
      es.(1)#set_text (try input_line fi with End_of_file -> "");
      close_in fi

let load_info () =
   let file_chooser = file_chooser_dialog `OPEN () in
      file_chooser#add_button_stock `CANCEL `CANCEL;
      file_chooser#add_select_button_stock `OPEN `ACCEPT;
      if file_chooser#run () = `ACCEPT then
         match file_chooser#filename with
              None -> ()
            | Some fn -> load_info_from_file fn
      else
         file_chooser#destroy ();
      file_chooser#destroy ();
      ();;

w#show ();;
w#connect#destroy ~callback:Main.quit;;
b_greet#connect#clicked ~callback:print_name;;
file_item#set_submenu file_menu;;
load_item#connect#activate ~callback:load_info;;
save_item#connect#activate ~callback:save_info;;
quit_item#connect#activate ~callback:w#destroy;;
Main.main ()


What's with the box?

code:
let master_vbox = vbox ~packing:w#add ()


In GTK apps, a menubar is essentially just another widget which gets packed like any other. So we'll create a vertical box which can hold the menu, and then everything else we've previously created.

Creating the menubar

code:
let m = menu_bar ~packing:master_vbox#pack ()


Pretty straightforward. Moving on...

The File menu

code:
let file_menu = menu ()


Again, pretty straightforward. This is just a container for menu items.

code:
let file_item = menu_item
   ~label:"File"
   ~packing:m#append
   ()


This may seem odd, but this is the actual "File" you click on in the menubar. It gets packed into the menubar with the "append" method.

Creating menu items

code:
let load_item = image_menu_item
   ~stock:`OPEN
   ~packing:file_menu#append
   ()

let save_item = image_menu_item
   ~stock:`SAVE
   ~packing:file_menu#append
   ()

let sep_item = separator_item ~packing:file_menu#append ()

let quit_item = image_menu_item
   ~stock:`QUIT
   ~packing:file_menu#append
   ()


The above is pretty straightforward. It creates the various items in the File menu. In many cases we use stock menu items, complete with stock icons.

Connecting the "File" with the actual menu

code:
file_item#set_submenu file_menu


Here we actually make the "File" menu item in the menubar bring up the file menu.

Making menu items do something

code:
load_item#connect#activate ~callback:load_info;;
save_item#connect#activate ~callback:save_info;;
quit_item#connect#activate ~callback:w#destroy;;


As we have with buttons, we need to connect an event to a callback function. These are pretty straightforward.

Refining the save process

In the applications you're probably used to, there are usually two different save options. If you have a file open, just clicking "save" will save the changes back to that file. Clicking "Save as", though will prompt you for a new filename.

Currently our "save" works just like the "save as". So we need to keep track of the file that's open at any given time.

code:
open GWindow
open GMain
open GButton
open GMisc
open GEdit
open GPack
open GMenu
open Gtk.Tags;;

let current_filename = ref ""

let w = window ~title:"Save As Test" ()

let master_vbox = vbox ~packing:w#add ()

let m = menu_bar ~packing:master_vbox#pack ()

let file_menu = menu ()

let file_item = menu_item
   ~label:"File"
   ~packing:m#append
   ()

let load_item = image_menu_item
   ~stock:`OPEN
   ~packing:file_menu#append
   ()

let save_item = image_menu_item
   ~stock:`SAVE
   ~packing:file_menu#append
   ()

let save_as_item = image_menu_item
   ~stock:`SAVE_AS
   ~packing:file_menu#append
   ()

let sep_item = separator_item ~packing:file_menu#append ()

let quit_item = image_menu_item
   ~stock:`QUIT
   ~packing:file_menu#append
   ()

let vb = vbox
   ~spacing:4
   ~border_width:5
   ~packing:master_vbox#pack
   ()

let n = notebook ~packing:vb#pack ()

let t = table
   ~rows:2
   ~columns:2
   ~packing:(n#append_page ~tab_label:(label ~text:"Name" ())#coerce)
   ~col_spacings:4
   ~row_spacings:4
   ~border_width:5
   ()
 
let hb_bottom = hbox
   ~spacing:4
   ~packing:(vb#pack ~from:`END)
   ()

let ls = [|
   label
      ~text:"First:"
      ~packing:(t#attach ~top:0 ~left:0 ~expand:`NONE)
      () ;
   label
      ~text:"Last:"
      ~packing:(t#attach ~top:1 ~left:0 ~expand:`NONE)
      () |]

let es = [|
   entry
      ~packing:(t#attach ~top:0 ~left:1 ~expand:`X)
      () ;
   entry
      ~packing:(t#attach ~top:1 ~left:1 ~expand:`X)
      () |]

let b_greet = button
   ~label:"Greet"
   ~packing:(hb_bottom#pack ~from:`END ~expand:false ~fill:false)
   ()

let print_name () =
   let fname = es.(0)#text and
       lname = es.(1)#text and
       make_dialog ~name =
          let d = message_dialog
             ~message:("Hello, " ^ name)
             ~message_type:`INFO
             ~title:"Hello!"
             ~buttons:Buttons.close
             ()
          in
             d#connect#response ~callback:(fun _ -> d#destroy ());
             d in
   let show_dialog ~name =
          let d = make_dialog ~name in
             d#show ()
   in
      match (fname, lname) with
           ("", "") -> ()
         | ("", n) -> show_dialog n
         | (n, "") -> show_dialog n
         | (m, n) -> show_dialog (m ^ " " ^ n)

let save_info_to_file filename =
   let fname = es.(0)#text
   and lname = es.(1)#text
   and fo = open_out filename in
      output_string fo (fname ^ "\n");
      output_string fo (lname ^ "\n");
      flush fo;
      close_out fo;
      current_filename := filename

let save_info () =
   let file_chooser = file_chooser_dialog `SAVE () in
      file_chooser#add_button_stock `CANCEL `CANCEL;
      file_chooser#add_select_button_stock `SAVE `ACCEPT;
      if file_chooser#run () = `ACCEPT then
         match file_chooser#filename with
              None -> ()
            | Some fn -> save_info_to_file fn
      else
         file_chooser#destroy ();
      file_chooser#destroy ();
      ()

let load_info_from_file filename =
   let fi = open_in filename in
      es.(0)#set_text (try input_line fi with End_of_file -> "");
      es.(1)#set_text (try input_line fi with End_of_file -> "");
      close_in fi;
      current_filename := filename

let load_info () =
   let file_chooser = file_chooser_dialog `OPEN () in
      file_chooser#add_button_stock `CANCEL `CANCEL;
      file_chooser#add_select_button_stock `OPEN `ACCEPT;
      if file_chooser#run () = `ACCEPT then
         match file_chooser#filename with
              None -> ()
            | Some fn -> load_info_from_file fn
      else
         file_chooser#destroy ();
      file_chooser#destroy ();
      ();;

w#show ();;
w#connect#destroy ~callback:Main.quit;;
b_greet#connect#clicked ~callback:print_name;;
file_item#set_submenu file_menu;;
load_item#connect#activate ~callback:load_info;;
save_item#connect#activate ~callback:(fun () ->
   if !current_filename = "" then
      save_info ()
   else
      save_info_to_file !current_filename);;
save_as_item#connect#activate ~callback:save_info;;
quit_item#connect#activate ~callback:w#destroy;;
Main.main ()


First off, a new variable

code:
let current_filename = ref ""


This variable holds a reference to a string which represents the currently open file. It starts out as an empty string, signifying that no file is open. The fact that this is a reference means we can change it later in the program. This capability will be important as we go on.

A new menu item

code:


let save_as_item = image_menu_item
   ~stock:`SAVE_AS
   ~packing:file_menu#append
   ()


Of course, we have to add a new menu item to the file menu for this purpose. Nothing too dramatic here.

A few functions change a bit, but not much

code:
let save_info_to_file filename =
   let fname = es.(0)#text
   and lname = es.(1)#text
   and fo = open_out filename in
      output_string fo (fname ^ "\n");
      output_string fo (lname ^ "\n");
      flush fo;
      close_out fo;
      current_filename := filename


code:
let load_info_from_file filename =
   let fi = open_in filename in
      es.(0)#set_text (try input_line fi with End_of_file -> "");
      es.(1)#set_text (try input_line fi with End_of_file -> "");
      close_in fi;
      current_filename := filename


For each of these functions we add a single line of code. At the end of each, we update the current-filename to be the same as the filename given to the function. If we're told to save info to a file, then that's the new current file. If we're told to load info from a particular file, then that becomes the new current file.

Event handlers have to change slightly

code:
save_item#connect#activate ~callback:(fun () ->
   if !current_filename = "" then
      save_info ()
   else
      save_info_to_file !current_filename)


The save item must now work a bit differently. If there is no current filename to work with, it must prompt for a filename. But, if there is a current filename, it should simply write the changes to that file without delay.

code:
save_as_item#connect#activate ~callback:save_info


The save as item simply prompts for a filename every time.

Work in progress

This post is a work in progress and will be revisited in the near future when I finish my own current experiment with GTK and O'Caml. Smile
Sponsor
Sponsor
Sponsor
sponsor
rizzix




PostPosted: Fri Jun 03, 2005 12:57 pm   Post subject: (No subject)

this is very much similar to swing. except in swing you dont have the additional: Main.main ()

its the way the java threading framework is designed..
wtd




PostPosted: Fri Jun 03, 2005 2:29 pm   Post subject: (No subject)

rizzix wrote:
this is very much similar to swing. except in swing you dont have the additional: Main.main ()

its the way the java threading framework is designed..


Yes, but there is an event loop there somewhere, even if it's implicit, rather than explicit.
rizzix




PostPosted: Fri Jun 03, 2005 6:42 pm   Post subject: (No subject)

as i said its the way the java threading framework is designed... the "void run();" method of a class with the Runnable interface is called for the event loop.
wtd




PostPosted: Sun Jun 05, 2005 1:30 am   Post subject: (No subject)

Just a notice: new stuff. I've been editing the original post, so it won't show up as updated. Smile
Display posts from previous:   
   Index -> Programming, General Programming -> Functional Programming
View previous topic Tell A FriendPrintable versionDownload TopicSubscribe to this topicPrivate MessagesRefresh page View next topic

Page 1 of 1  [ 5 Posts ]
Jump to:   


Style:  
Search: