使用Perl编写GNOME应用程序 - 第2部分

目录

菜谱应用程序
主屏幕
列式列表
显示菜谱
我们现在在哪里,我们将走向何方
上篇文章的说明

上个月的文章探讨了如何使用Gtk+和GNOME创建一个简单的“Hello World”应用程序。这个月,我们将构建一个更复杂的应用程序 - 一个用于存储和检索菜谱的应用程序。

菜谱应用程序

在我们写下任何代码之前,让我们看看我们将如何设计这个应用程序。首先,我们将查看用户界面,然后看看这对我们的程序设计意味着什么。

在设计用户界面时,我们需要考虑如何为用户提供最有用、最直观的数据视图,而不会让他们感到信息过载。当我们使用应用程序时,我们能够轻松获取什么?这个问题有两个部分:我们可以执行的操作和我们可以看到的数据。

就数据而言,我认为将可用的菜谱组织成一个列表是最好的方式,就像菜谱书中的目录一样;通过上下滚动列表来查看菜谱标题,然后点击一个标题来显示整个菜谱。我们也可以在每个标题旁边显示一些有用的信息。我认为最有用的信息是烹饪时间和添加菜谱的日期。

现在我们可以看看将要执行的操作 - 这些将转换为工具栏按钮。我最希望的一个功能是能够为程序提供一个我拥有的食材列表,并让程序告诉我可以用这些食材做什么。我还希望能够维护多个不同的菜谱集,所以“保存”和“打开”是自然的选择。当然,你需要能够添加新的菜谱,所以一个“添加”按钮也会很有用。请注意,我不想有一个“删除”按钮 - 删除菜谱可能很少发生,即使那样,你也不想让它变得太容易。

这就是主屏幕的界面,下面是这个界面的样子

现在我们可以考虑我们需要存储的数据。我们需要存储带有标题、日期和烹饪时间的菜谱。如果我们想按成分搜索,我们还应该存储每个菜谱所需的成分。拥有我们所知道的所有成分的完整列表也会很有用,我们还将有一些用户配置设置。

最初,我考虑将菜谱放入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小部件处理起来有点不友好:你可以向其中添加数据,但无法找出列表中的内容,因此你必须维护一个包含数据的单独数组;当点击列标题时,列列表通常会重新排序,但程序员必须自己处理这种情况;数据必须通过列号引用,而不是通过列名引用;等等。

既然我意识到每次我想获取列列表时都会很麻烦,我就编写了一个名为Gtk::HandyCList的模块,它封装了所有这些特性。(如果您想尝试这个,您需要从CPAN下载该模块。请确保您获取版本0.02,因为我们下面使用的是hide方法,这是该版本中新增的。)

为了将其添加到我们的程序中,我们首先需要要显示的数据!让我们创建一个类似这样的模拟数据数组

        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' );

现在我们创建HandyCList。首先,我们指定将要使用的列名,然后我们设置每列的大小。

  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_datedisplay_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对象,它会告诉我们点击的类型。在我们的例子中,我们只想对双击做出响应,此时类型为"2button_press"。如果不是这种情况,我们就返回。

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

既然我们知道接收信号的行,我们就可以通过data方法从HandyCList中提取该行。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);

对话框由两个区域组成:底部的一个“操作区域”,其中应包含可用的“操作”或按钮,以及顶部的一个vbox,我们将我们的消息放入其中。因此,我们将按钮打包到操作区域,并将我们的Less小部件打包到vbox中。

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

最后,我们告诉分页器它应该显示的字符串,然后显示对话框。现在我们可以显示菜谱了。

我们现在的位置,以及我们的目标

到目前为止,应用程序的完整源代码可以在这里找到。到目前为止,我们只处理了静态数据,这是硬编码到应用程序中的,这不是一个真实的场景。下次,我们将探讨添加和删除菜谱,以及使用XML将菜谱保存和恢复到磁盘。完成后,我们将有一个基本菜谱应用程序的核心。在本教程的最后一部分,我们将添加更多功能,例如按成分搜索。

上一篇文章的注释

上个月的文章发表后,有几个人给我写信说他们无法让应用程序的GNOME版本工作;如果这是问题,您需要使用Gnome.pm模块的最新版本。CPAN上的那个不是最新的——相反,您应该使用Gnome.pm网站上的版本,网址为http://projects.prosa.it/gtkperl

我还因为说“GNOME是Unix桌面”而受到了批评。公平起见——另一个为Unix提供类似环境的开源项目是KDE,但长期以来它受到了开发人员对TrollTech及其QPL许可证的怀疑的限制。与此同时,像Sun和IBM这样的巨头正在向GNOME基金会投入资金,使GNOME成为Unix桌面,所以这样说似乎是合理的。

现在,大多数人很高兴看到同样的巨头也建立了KDE联盟。(从http://www.kde.org/announcements/gfresponse.html: “现在我们被问及‘KDE是否将创建一个与GNOME基金会具有相同意义的KDE基金会?’答案是绝对不。’你们说得很好。)KDE看起来是GNOME的有力替代品。显然,我更喜欢GNOME,但正如http://segfault.org所说:“KDE - GNOME战争——到目前为止的伤亡:0”。

标签

反馈

这篇文章有什么问题吗?请帮助我们通过在GitHub上打开一个问题或拉取请求来解决问题。