Gtk2::Helper
- Simple Chat This lesson will build on our newly acquired knowledge of Gtk2::Helper
. We will create a simple chat program. We will introduce a few nice ticks to give the user a more pleasant time communicating.
The chat program needs the following requirements:
Two people must be able to communicate with each other over a TCP/IP network.
To add an aesthetic touch, the GUI must be given a theme.
When the two persons chat, the program should be able to function without the use of a mouse.
A scrollable history is needed for the duration of the chat session, as new postings arrive, it should become visible.(The latest postings must always be visible)
The program can be found here: 'Chat program'.
1 #!/usr/bin/perl -w 2 3 use strict; 4 use IO::Socket; 5 6 use Gtk2 -init; 7 use Glib qw/TRUE FALSE/; 8 use Gtk2::Helper; 9 use Gtk2::Pango; 10 use Gtk2::Gdk::Keysyms; 11 12 my ($who) = $ARGV[0]; 13 #check and warn if we received wrong argument 14 unless(($who)&&($who =~ m/shrek|donkey/)){ 15 print "usage <program name> <shrek | donkey>\n"; 16 exit; 17 } 18 19 my($sock,$MAXLEN, $LISTEN_PORT, $SEND_PORT,$tag_send,$tag_receive,$img_big,$img_send,$img_rec); 20 $MAXLEN = 1024; 21 22 #set the variables up if you are shrek: 23 if ($who =~ m/shrek/){ 24 25 $LISTEN_PORT = 5151; 26 $SEND_PORT = 5152; 27 $tag_send = 'shrek'; 28 $tag_receive = 'donkey'; 29 $img_big = './pix/shrek.gif'; 30 $img_send = './pix/shrek_small.jpg'; 31 $img_rec = './pix/donkey_small.gif'; 32 33 } 34 35 #set the variables up if you are donkey: 36 if ($who =~ m/donkey/){ 37 38 $LISTEN_PORT = 5152; 39 $SEND_PORT = 5151; 40 $tag_send = 'donkey'; 41 $tag_receive = 'shrek'; 42 $img_big = './pix/donkey.gif'; 43 $img_send = './pix/donkey_small.gif'; 44 $img_rec = './pix/shrek_small.jpg'; 45 } 46 47 my $tview; 48 49 #--------------------------------- 50 #set up a udp server waiting for incomming messages 51 $sock = IO::Socket::INET->new(LocalPort => $LISTEN_PORT, Proto => 'udp') 52 or die "socket: $@"; 53 54 #add a Gtk2::Helper watch on any incomming connections 55 Gtk2::Helper->add_watch ( fileno $sock, 'in',sub{ 56 my ($fd,$condition,$fh) = @_; 57 #call 'watch_callback' to handle the incomming data 58 \&watch_callback($fh,$tview); 59 },$sock); 60 61 print "Awaiting UDP messages on port $LISTEN_PORT\n"; 62 #--------------------------------- 63 64 #Gtk2::Rc->parse ('/usr/share/themes/Anger/gtk/gtkrc'); 65 #this is parsing our theme file, giving a personal touch 66 Gtk2::Rc->parse ('gtkrc'); 67 68 #standard window creation, placement, and signal connecting 69 my $window = Gtk2::Window->new('toplevel'); 70 $window->signal_connect('delete_event' => sub { exit;}); 71 $window->set_border_width(5); 72 $window->set_position('center_always'); 73 #again just some fine touches 74 $window->set_title("I am $tag_send"); 75 $window->set_icon_from_file($img_send); 76 77 #this vbox will geturn the bulk of the gui 78 my ($vbox) = &ret_vbox(); 79 80 #add and show the vbox 81 $window->add($vbox); 82 $window->show(); 83 84 #our main event-loop 85 Gtk2->main(); 86 87 sub ret_vbox { 88 89 my $vbox = Gtk2::VBox->new(FALSE,0); 90 #add an image to indicate who you are 91 my $img_who = Gtk2::Image->new_from_file($img_big); 92 93 $vbox->pack_start($img_who,TRUE,TRUE,0); 94 95 my $frame = Gtk2::Frame->new("Simple Chat - I am $tag_send"); 96 97 #method of Gtk2::Container 98 $frame->set_border_width(5); 99 100 my $sw = Gtk2::ScrolledWindow->new (undef, undef); 101 $sw->set_shadow_type ('etched-out'); 102 $sw->set_policy ('automatic', 'automatic'); 103 #This is a method of the Gtk2::Widget class,it will force a minimum 104 #size on the widget. Handy to give intitial size to a 105 #Gtk2::ScrolledWindow class object 106 $sw->set_size_request (300, 300); 107 #method of Gtk2::Container 108 $sw->set_border_width(5); 109 110 $tview = Gtk2::TextView->new(); 111 #we do not want to edit anything 112 $tview->set_editable(FALSE); 113 $tview->set_cursor_visible (FALSE); 114 115 my $buffer = $tview->get_buffer(); 116 117 #create a mark at the end of the buffer, and on each 118 #'insert_text' we tell the textview to scroll to that mark 119 $buffer->create_mark ('end', $buffer->get_end_iter, FALSE); 120 $buffer->signal_connect (insert_text => sub { 121 $tview->scroll_to_mark ($buffer->get_mark ('end'), 122 0.0, TRUE, 0, 0.5); 123 }); 124 125 #create a tag for the shreck 126 $buffer->create_tag ("shrek", 127 style =>'italic', 128 weight => PANGO_WEIGHT_ULTRALIGHT, 129 family => 'flubber', 130 foreground => "#189f3b", 131 size => 20000, 132 ); 133 134 #create a tag for the donkey 135 $buffer->create_tag ("donkey", 136 style =>'italic', 137 weight => PANGO_WEIGHT_ULTRALIGHT, 138 family => 'davis', 139 foreground => "blue", 140 size => 20000, 141 ); 142 143 $sw->add($tview); 144 $frame->add($sw); 145 $vbox->pack_start($frame,TRUE,TRUE,4); 146 #-------------------------------------- 147 my $hbox = Gtk2::HBox->new(FALSE,5); 148 149 my $ent_send = Gtk2::Entry->new; 150 $hbox->pack_start($ent_send,TRUE,TRUE,0); 151 152 my $btn_send = Gtk2::Button->new_from_stock('gtk-ok'); 153 #connect the 'key_press_signal' to a handler that will 154 #filter for 'Return'; if TRUE, trigger a button click 155 $ent_send->signal_connect('key_press_event'=> sub { 156 my ($widget,$event) = @_; 157 if($event->keyval == $Gtk2::Gdk::Keysyms{Return}) { 158 $btn_send->clicked; 159 return 1; 160 } 161 162 }); 163 164 $btn_send->signal_connect("clicked" =>sub { 165 #get the contents of the entry 166 my $msg_send = $ent_send->get_text; 167 #clear the entry 168 $ent_send->set_text(""); 169 #grab focus again for the next round of talks 170 $ent_send->grab_focus; 171 #if there was bogus input, ignore it! 172 if ($msg_send !~ m/^\s*$/){ 173 #open up a UDP client connection 174 my $server_host = "127.0.0.1"; 175 my $sock = IO::Socket::INET->new(Proto => 'udp', 176 PeerPort => $SEND_PORT, 177 PeerAddr => $server_host) 178 or die "Creating socket: $!\n"; 179 #send the message out on the socket 180 $sock->send($msg_send."\n") or die "send: $!"; 181 #update the screen locally 182 &update_buffer($buffer,$msg_send,TRUE); 183 } 184 }); 185 186 $hbox->pack_end($btn_send,TRUE,TRUE,0); 187 #-------------------------------------- 188 $vbox->pack_start($hbox,TRUE,TRUE,4); 189 #set initial focus 190 $vbox->set_focus_child($hbox); 191 192 $vbox->show_all(); 193 return $vbox; 194 } 195 196 197 sub watch_callback { 198 199 my ($fh,$tview) = @_; 200 my $msg; 201 $fh->recv($msg, $MAXLEN) or die "recv: $!"; 202 print $msg."\n"; 203 my $buffer = $tview->get_buffer(); 204 &update_buffer($buffer,$msg,FALSE); 205 206 return 1; 207 } 208 209 Gtk2->main; 210 211 sub update_buffer { 212 213 my ($buffer,$msg,$send)= @_; 214 215 $msg = $msg."\n"; 216 if ($send) { 217 218 my $iter = $buffer->get_end_iter; 219 $buffer->insert_pixbuf ($iter, Gtk2::Gdk::Pixbuf->new_from_file ($img_send)); 220 $buffer->insert_with_tags_by_name($iter, $msg,$tag_send); 221 222 }else{ 223 224 my $iter = $buffer->get_end_iter; 225 $buffer->insert_pixbuf ($iter, Gtk2::Gdk::Pixbuf->new_from_file ($img_rec)); 226 $buffer->insert_with_tags_by_name($iter, $msg,$tag_receive); 227 228 } 229 }
We choose two famous characters from the movie "Shrek" to represent the chatters in our program.
This is the core function of the program, the rest is just nice ways of presenting it. We use the IO::Socket
Perl module. We use UPD instead of TCP because it is simpler and faster. When we specify "shrek" as an argument to the program, it will set up a certain set of variables, specific to "shrek". If we specify "donkey" as an argument to the program, it will set up a certain set of variables, specific to "donkey"
[1]
Depending on the chosen variable values, it will set up a UDP server on a specified port and make use of Gtk2::Helper
to wait for any incoming messages. Note that our filehandle ($fh) is now in the form of a network socket ($sock).
#--------------------------------- #set up a udp server waiting for incoming messages $sock = IO::Socket::INET->new(LocalPort => $LISTEN_PORT, Proto => 'udp') or die "socket: $@"; #add a Gtk2::Helper watch on any incoming connections Gtk2::Helper->add_watch ( fileno $sock, 'in',sub{ my ($fd,$condition,$fh) = @_; #call 'watch_callback' to handle the incoming data \&watch_callback($fh,$tview); },$sock); print "Awaiting UDP messages on port $LISTEN_PORT\n"; #---------------------------------
We use a Gtk2::Entry
widget to type our posting into. When we want to send the message, we click on the "OK" button. This will set up a UDP connection to the listening server, and send the message out on that network socket.
This chat demo is talking to IP "127.0.0.1", which is the IP of the local machine. Should you want to talk to a different IP, just change it in code, or as an exercise, add an |
#open up a UDP client connection my $server_host = "127.0.0.1"; my $sock = IO::Socket::INET->new(Proto => 'udp', PeerPort => $SEND_PORT, PeerAddr => $server_host) or die "Creating socket: $!\n"; #send the message out on the socket $sock->send($msg_send."\n") or die "send: $!";
To do this, we make use of an "rc" file. [2] This is like a style sheet, which Gtk+ use to format the appearance of specified widgets. Every modern Linux distribution will have default "rc" files that is used by Gtk+. This will give programs an universal look and feel. In our program we specify an extra "rc" file to use:
#this is parsing our theme file, giving a personal touch Gtk2::Rc->parse ('gtkrc');
The rc file we use was taken from the "Anger" theme and modified.
It is always helpful to look at existing "rc" files when creating your own. |
Remember that rc files are not using Perl. The class names are the standard Gtk+ names. EG GtkButton VS |
bg_pixmap[NORMAL] = "./pix/bg.png"This will fill the background of the window with tiled images of the image file specified.
#create a style for the enrty where we #enter our text style "ent" { font_name = "Times Bold 15" base[NORMAL] = "blue" text[NORMAL] = "red" } #------------------------------------ #Bind this style to any GtkEnry class class "GtkEntry" style "ent" #-------------------------------------The above piece will change the background and the text color of all the GtkEntry widgets. It will also change the font. "font_name" uses the more friendly Pango format. This is recomended above "font" which uses the more complex XLFD font description.
#create a style for the label inside a GtkFrame style "frame_label" = "default" { font_name = "Times Bold 20" fg[NORMAL] = "#1d6629" } #------------------------------------ #Bind this style to any GtkFrame's GtkEnry. widget_class "*.GtkFrame.GtkLabel" style "frame_label" #-------------------------------------When you want to change the text of a GtkFrame, remember, the text is actually part of a GtkLabel inside the GtkFrame widget! If you forget this, you may become frustrated, as nothing will happen when you bind the style to GtkFrame. Use the
widget_class
binding form for this to avoid changing ALL GtkLabel widgets by accident.
To change the GtkButton, we made use of the "image" theme engine. The theme engines can be quite cryptic and may not be well documented. The best advice is to investigate existing uses of them to gain a better understanding.
To do this we give focus to Gtk2::Entry
on initial startup.
When the program starts we set the initial focus to the Gtk2::HBox
. This will default to the Gtk2::Entry
because it was packed into the Gtk2::HBox
first. Gtk2::Entry
is now ready to accept input from the keyboard.
#set initial focus $vbox->set_focus_child($hbox);When the button is clicked, the focus moves from the
Gtk2::Entry
to the button. In the signal handling code, we have to move the focus on the button back to the Gtk2::Entry
. This will allow us to start entering our next message into the Gtk2::Entry
after we clicked the button.
#clear the entry $ent_send->set_text(""); #grab focus again for the next round of talks $ent_send->grab_focus;
The keyboard can be used to trigger a "click" on the Gtk2::Button
.
Use of the key_press_event
signal of Gtk2::Entry
. Connect it to a closure that will cause the click
method of Gtk2::Button
to be called whenever the "return" key is pressed.
$ent_send->signal_connect('key_press_event'=> sub { my ($widget,$event) = @_; if($event->keyval == $Gtk2::Gdk::Keysyms{Return}) { $btn_send->clicked; return 1; } });
To do this, we put a Gtk2::TextMark
at the end of the Gtk2::TextBuffer
and call the scroll_to_mark
method of the Gtk2::TextView
each time text is inserted into the Gtk2::TextBuffer
.
#create a mark at the end of the buffer, and on each #'insert_text' we tell the textview to scroll to that mark $buffer->create_mark ('end', $buffer->get_end_iter, FALSE); $buffer->signal_connect (insert_text => sub { $tview->scroll_to_mark ($buffer->get_mark ('end'), 0.0, TRUE, 0, 0.5); });
[1] | Networking falls outside the scope of this book. To get extra information, you can "google" for phrases like "perl socket demo" or "perl socket tutorial". "socket" can also be replaced by "network". There are quite a number of good tutorials available to gain background knowledge. |
[2] | Should you require more info on the format of rc files: Appendix C |