Programming GNOME Applications with Perl-Part 2

取自 PerlChina.org - wiki

跳转到: 导航, 搜索
  • 翻 译:SaladJonk
  • 出 处:中国 Perl 协会 FPC - PerlChina.org
  • 原 名:Perl_com Programming GNOME Applications with Perl-Part 2
  • 中 文:用 Perl 进行 GNOME 编程-第二部分
  • 作 者:Simon Cozens
  • 原 文: http://www.perl.com/lpt/a/489
  • 发 表:2000-11-28


目录


上个月的文章,我们主要讨论了如何用 GTK+ 和 GNOME 创建一个简单的 “Hello World ” 程序。这次,我们将创建一个复杂一些的程序,一个能保存和提取食谱的程序。

[编辑] 菜谱管理程序

在我们开始写程序前,先让我们来看看怎么设计它。首先设计用户界面,然后再考虑如何用程序实现这个界面。

当设计用户界面的时候,我们要考虑怎样给用户提供一个有用又直观的浏览数据的界面,这个界面不能太拥挤,给人压抑感。当我们使用一个软件的时候,怎样才能用起来简单顺手呢?这个问题涉及两方面:软件能提供的交互手段和我们能看到的数据。

从数据显示方面考虑,我觉得最好的组织这些菜谱的方法是把他们弄成一个列表,就像真正的菜谱书上印刷出的表格一样。上移和下移滚动条可以浏览每道菜的名字,点击名字则就可以看到相应那道菜的全部内容。我们还可以在名字后面附加一些其它的信息,我觉得该条该道菜被添加的日期和烹饪它需要的时间是比较有用的内容。

然后再来看有什么交互功能,这得由工具栏上的按钮决定。我觉得程序应该实现的一个很有用的功能是:由用户输入手头上有的原料,然后程序可以告诉用户这些原料可以烹饪出哪些道菜。程序还应该能把我们输入的菜谱保存起来,以后能再次打开,所以 "Save" 和 "Open" 按钮是必要的。当然,添加新菜谱的功能是必须的,所以,还应该有 "Add" 按钮,注意 "Delete" 按钮是不太需要的,因为我们很少会删除已经输入的菜谱,即使你要添加这个功能,也很简单。最后,我们还需要一个退出的按钮。

以上说的就构成程序的主界面,它看起来应该像这样:

Image:Gnome-main1.jpg

现在,我们再来考虑数据的存储问题。我们需要保存每道菜的名字,输入日期和烹饪它需要的时间。如果我们想通过原料来搜索可以做哪些菜,那么还应该将原料存起来。浏览所存储的原料列表要方便,还要允许用户可以自定义一些东西,实现个性化。

最初,我打算把所有的食谱都放到一个 SQL 关系型数据库中,但最后由于两个原因放弃了:首先,把每道菜的名字和需要的原料关联起来,会带来一些不必要的复杂性。其次,GNOME 应用程序通常是把数据保存在XML文档中,这样能在很容易在不同程序间共享。所以,最后我决定把用户自定义的选项和原料的清单都放在一个 XML 文档中,然后把整个菜谱放在一个单独的文件里。

[编辑] 主界面

现在我们已经设计好了程序的主界面,可以实现它了。像以前的程序一样,我们首先实现程序的菜单条和工具栏。

 #!/usr/bin/perl -w
        use strict;
        use Gnome;

        my $NAME    = 'gCookBook';
        my $VERSION = '0.1';

        init Gnome $NAME;

        my $app = new Gnome::App $NAME, $NAME;
		
        signal_connect $app 'delete_event', 
          sub { Gtk->main_quit; return 0 };

        $app->create_menus(
           {
          type => 'subtree',
          label => '_File',
          subtree => [
                { 
                 type => 'item',
                 label => '_New',
                 pixmap_type => 'stock',
                 pixmap_info => 'Menu_New'
                },
                {
                 type => 'item',
                 label => '_Open...',
                 pixmap_type => 'stock',
                 pixmap_info => 'Menu_Open'
                },
                {
                 type => 'item',
                 label => '_Save',
                 pixmap_type => 'stock',
                 pixmap_info => 'Menu_Save'
                },
                {
                 type => 'item',
                 label => 'Save _As...',
                 pixmap_type => 'stock',
                 pixmap_info => 'Menu_Save As'
                },
                {
                 type => 'separator'
                },
                {
                 type => 'item',
                 label => 'E_xit',
                 pixmap_type => 'stock',
                 pixmap_info => 'Menu_Quit',
                 callback => sub { Gtk->main_quit; return 0 }
                }
                 ]
           },
           { 
          type => 'subtree',
          label => '_Edit',
          subtree => [
                {
                 type => 'item',
                 label => 'C_ut',
                 pixmap_type => 'stock',
                 pixmap_info => 'Menu_Cut',
                },
                {
                 type => 'item',
                 label => '_Copy',
                 pixmap_type => 'stock',
                 pixmap_info => 'Menu_Copy'
                },
                {
                 type => 'item',
                 label => '_Paste',
                 pixmap_type => 'stock',
                 pixmap_info => 'Menu_Paste'
                }
                 ]
           },
           {
          type => 'subtree',
          label => '_Settings',
          subtree => [
                {
                 type => 'item',
                 label => '_Preferences...',
                 pixmap_type => 'stock',
                 pixmap_info => 'Menu_Preferences',
                 callback => \&show_prefs
                }
                 ]
           },
           {
          type   => 'subtree',
          label  => '_Help',
          subtree => [
                {type => 'item', 
                 label => '_About...',
                 pixmap_type => 'stock',
                 pixmap_info => 'Menu_About',
                 callback => \&about_box
                }
             ]
           }
          );

    $app->create_toolbar(
           {
            type     => 'item',
            label    => 'Cook',
            pixmap_type => 'stock',
            pixmap_info => 'Search',
            hint     => 'Find a recipe by ingedients'
           },
           {
            type     => 'item',
            label    => 'Add',
            pixmap_type => 'stock',
            pixmap_info => 'Add',
            hint     => 'Add a new recipe'
           },
           {
            type     => 'item',
            label    => 'Open...', 
            pixmap_type => 'stock',
            pixmap_info => 'Open',
            hint     => "Open a recipe book"
           },
           {
            type     => 'item',
            label    => 'Save', 
            pixmap_type => 'stock',
            pixmap_info => 'Save',
            hint     => "Save this recipe book"
           },
           { 
            type     => 'item',
            label    => 'Exit',
            pixmap_type => 'stock',
            pixmap_info => 'Quit',
            hint     => "Leave $NAME",
            callback  => sub { Gtk->main_quit;}
           }
          );

    $app->set_default_size(600,400);

    my $bar = new Gnome::AppBar 0,1,"user" ;
    $bar->set_status("");
    $app->set_statusbar( $bar );

    show_all $app;

    main Gtk;

    sub about_box {
      my $about = new Gnome::About $NAME, $VERSION,
      "(C) Simon Cozens, 2000", ["Simon Cozens"], 
      "This program is released under the 
          same terms as Perl itself";
      show $about;
      }

[编辑] 列表

接着,我们应该显示出每道菜的名字。通常用 CList 控件实现。然而,标准的 Gtk 库中的 CList 控件不太好用,你只能向里面输入数据,而不能读取数据。所以你不得不把数据单独放到一个数组里面。CList 控件提供其下列表的自动排序,你只需要在列表名栏中点击一下就可以了,

我觉得每次要弄列表栏都不太方便,于是我自己写了一个模块,叫 Gtk::HandyClist,它封装了上面提到的所有功能。(你如果你想试试这个模块,可以从 CPAN 上下载它,注意要下0.02版,因为我们后面用到的 hide 方法只有在 0.02 版中才有)

为了实现这个列表,首先要准备好待显示的数据!像下面这样创建一个比较抽象的数组:

my @cookbook = (
                [ "Frog soup", "29/08/99", "12"],
                [ "Chicken scratchings", "12/12/99", "40"],
                [ "Pork with beansprouts in a garlic
                    butter sauce and a really really long name
                    that we have to scroll to see",
                  "1/1/99", 30],
                [ "Eggy bread", "10/10/10", 3]
               );

然后装载上面提到的模块:

use Gtk::HandyCList;

列表要可以上下滚动,所以我们把列表放到一个能处理滚动条的控件中:Gtk::ScrolledWindow。

my $scrolled_window = new Gtk::ScrolledWindow( undef, undef );
  $scrolled_window->set_policy( 'automatic', 'always' );

现在开始创建列表,首先,指定列表的名字,然后指定列表每行的长宽尺寸。

 my $list = new Gtk::HandyCList qw(Name Date Time);
  $list->sizes(350,150,100);

我们希望当点击列表的标题栏的时候能对列表进行排序,所以我们需要在模块中定义好排序的方法。如果你不定义,默认的方法可以对字母和数字进行排序,但是我们希望它能用我们提供的子函数排序。我们还给列表设置了阴影效果,看起来漂亮些。

$list->sortfuncs("alpha", \&sort_date, "number");
  $list->set_shadow_type('out');

现在把抽象数组定义的数据传给列表:

$list->data(@cookbook);

然后,把列表加到那个可滚动的窗口中,并告诉程序,最顶层的窗口是这个可滚动的窗口。

$scrolled_window->add($list);
  $app->set_contents($scrolled_window);

最后,我们还要处理当用户点击菜名的时候发来的信号,收到信号后显示这道菜的全部内容。

$list->signal_connect( "select_row", \&display_recipe);

当然,我们还需要实现上面用到的两个子程序,sort_data和display_recipe。后者现在暂时不考虑,先搞定前者。下面是我写的第一个子程序的实现代码,因为我是个英国人,所以程序风格有点英国化:

 sub sort_date {
          my ($ad, $am, $ay) = ($_[0] =~ m|(\d+)/(\d+)/(\d+)|);
          my ($bd, $bm, $by) = ($_[1] =~ m|(\d+)/(\d+)/(\d+)|);
          return $ay <=> $by || $am <=> $bm || $ad <=> $bd;
        }

至于程序风格的本地化就留给读者了。

到现在,你手头的这个程序已经可以分列显示出菜名,该道菜输入的时间,和烹饪它需要的时间了。运行它,点击标题栏看看自动排序的功能,然后把窗口的大小调整一下,看看会发生什么。

[编辑] 显示每道菜的详细内容

现在让我们来处理每道菜详细内容的显示问题。这个问题就有点复杂了。首先,我们必须要把每道菜的相关内容存起来,这里把菜名,录入时间,烹饪需要的时间都存在名为 @cookbook 的数组里,所以,这个原来创建的抽象数组每项内容都要加一列:

    my @cookbook = (
        [ "Frog soup", "29/08/99", "12", 
          "Put frog in water. Slowly raise water temperature 
           until frog is cooked."],
        [ "Chicken scratchings", "12/12/99", "40", 
          "Remove fat from chicken, and fry 
	   under a medium grill"],
        [ "Pork with beansprouts in a garlic butter sauce 
           and a really really long name that we have to
           scroll to see",
          "1/1/99", 30, 
	  "Pour boiling water into packet and stir"],
        [ "Eggy bread", "10/10/10", 3, 
	  "Fry bread. Fry eggs. Combine."]
           );

由于不需要一次就在列表框中显示出所有的数据,所以传给Gtk::HandyCList模块的数据要作修改:

- my $list = new Gtk::HandyCList qw(Name Date Time);
 + my $list = new Gtk::HandyCList qw(Name Date Time Recipe);
 + $list->hide("Recipe");

(如果你不记得上面的句型,这里说一下:前面带减号的行表示的内容将不被显示,而带加号的行表示的内容将被显示)。

现在,我们把每道菜的相关数据都保存在我们自己定义的数据结构中了,如果我们想要浏览这些数据怎么办呢?可以用 Gnome::Less 这个控件,它是根据 Unix 中的 less 实用程序命名的。本是一个文件浏览器,但我们也可以用它来浏览字符。

现在停一停,想一想下一步要做什么。我们需要捕捉用户双击菜名的信号,然后应该要弹出一个窗口,里面由 Gnome::Less 控件来显示用户所点击那道菜的详细内容。还要允许用户关闭这个弹出窗口。我们把鼠标点击的信号同 display_recipe 子函数绑定起来,下面是这个子函数的代码:

sub display_recipe {
      my ($clist, $row, $column, $mouse_event) = @_;
      return unless $mouse_event->{type} eq "2button_press";

当用户点击时,会收到信号传给子函数的参数。参数中的第一项指明发出信号的是哪个控件,这里是 HandyCList 控件。这一点将决定后面的参数内容。假如发出信号的是HandyCList 控件,那么后面的参数就是用户点击内容所在的行和列,及一个 Gtk::Gdk::MouseEvent 实例来指明用户点击的类型,是双击还是单击。这里,我们只处理双击的情况,对应的 Gtk::Gdk::MouseEvent 的值为"2button_press"。如果用户的动作不是双击,那么返回:

 my %recipe = %{($clist->data)[$row]};

如果我们知道点击处所在的行,我们就可以从 HandyCList 控件中,通过 data 方法提取出那行的内容。Data 方法是一个用来提取数据和输入数据的方法,用这个方法我们能很方便的存储数据和显示数据。我们也可以用它来显示列表中的数据,每一行都作为一个散列的引用被存储,然后我们根据引用找到数据。

my $recipe_str = $recipe{Name}."\n";
      $recipe_str .= "-" x length($recipe{Name})."\n\n";
      $recipe_str .= "Cooking time : $recipe{Time}\n";
      $recipe_str .= "Date created : $recipe{Date}\n\n";
      $recipe_str .= $recipe{Recipe};

接着,产生我们要显示的字符串,利用被引用的散列的值。

 my $db = new Gnome::Dialog($recipe{Name});
      my $gl = new Gnome::Less;
      my $button = new Gtk::Button( "Close" );
      $button->signal_connect( "clicked", sub { $db->destroy } );

目前为止,我们这个弹出窗口包含了三个部分:弹出的对话框(对话框的标题栏显示对应那到道菜的名称)的框架,对话框的页面,其上显示用户点击那道菜的具体内容,一个关闭按钮,当用户点击的时候,对话框会被关掉。

   $db->action_area->pack_start( $button, 1, 1, 0 );
      $db->vbox->pack_start($gl, 1, 1, 0);

弹出的对话框由两部分组成:包含动作的位于底部的动作域,也就是放按钮的位置,还有就是上部的显示信息的文本域。因此,我们把设计的按钮放到动作域,而显示数据的Less 控件放到文本域。

  $gl->show_string($recipe_str);
      show_all $db;
    }

最后,把要显示的字符串传给页面,然后显示对话框。菜谱的详细内容就被显示出来了

[编辑] 我们学到哪里了,下面的教程将讲解什么?

到现在,这个程序的完整代码,点击这里获得。

目前,我们还只是处理一些静态的数据,把它们硬编码进程序,而实际生活中这是行不通的。后面的教程我们将要介绍如何删除和添加菜谱,同时还将把所有的数据存在磁盘上的XML文件中。一旦所以的这些完成了,一个菜谱管理程序的核心部分就完成了,在教程的最后部分,我们还将加入更多的功能,比如根据输入原料来判断能烹饪那些菜品。

[编辑] 最后的注意事项

自从我上个月写了第一篇教程后,有几个朋友给我写了信,说到它们的 GNOME 版程序不能运行。如果你也遇到这个问题,那么你可能需要安装最新版的 Gnome.pm 模块。在CPAN上的模块不是最新的,在这个地址去下最新的 http://projects.prosa.it/gtkperl。

我支持“GNOME 是 Unix 的桌面系统”的说法,KDE 也很好的提供 UNIX 下相同的桌面环境,只是很长时间来人们因为它的 TrollTech 和 QPL 许可证而不得不放弃使用它。同时,像Sun和IBM等大公司都在投入资金开发 GNOME,力图使它成为 Unix 下的正统桌面系统,所以谁是 Unix 下桌面系统不难理解。

现在很多人都高兴的看到那些大公司也在为开发 KDE 的社团投钱。(引用http://www.kde.org/announcements/gfresponse.html上的话,“现在我们经常被问到 KDE 开发人员是否也会成立一个想 GNOME 基金一样的基金?答案是,不会,绝对不会。“) KDE是除了 GNOME 的另一个好选择,很明显,我更喜欢 GNOME,但是像 http://segfault.org/上说到:”KDE-GNOME之间的战争,双方都还没输!)

个人工具