如何在Perl中构建一个简单的菜单?

时间:2014-08-22 01:48:20

标签: perl user-interface oop menu submenu

我正在处理需要一些基本菜单功能的Perl脚本。最后,我希望每个菜单都有几个选项,然后选择返回上一个菜单或退出。

示例:

这是一个菜单:

  1. 选择1
  2. 选择2
  3. 返回上一级菜单
  4. 退出
  5. 选择一个选项:

    我目前有一个菜单子程序来制作菜单,但是没有功能允许它返回上一个菜单。

        sub menu
        {
            for (;;) {
                print "--------------------\n";
                print "$_[0]\n";
                print "--------------------\n";
                for (my $i = 0; $i < scalar(@{ $_[1]}); $i++) {
                    print $i + 1, "\.\t ${ $_[1] }[$i]\n";
                }
                print "\n?: ";
                my $i = <STDIN>; chomp $i;
                if ($i && $i =~ m/[0-9]+/ && $i <= scalar(@{ $_[1]})) {
                    return ${ $_[1] }[$i - 1];
                } else {
                    print "\nInvalid input.\n\n";
                }
            }
        }
    
        # Using the menu
        my $choice1  = menu('Menu1 header', \@list_of_choices1);
    
        # I would like this menu to give the option to go back to
        # the first menu to change $choice1
        my $choice2 = menu('Menu2 header', \@list_of_choices2);
    

    我不想对所有菜单进行硬编码,并使用if / elsif语句进行所有处理,因此我将菜单转换为函数。

    我的菜单目前看起来像这样......

    菜单标题:

    1. 选择1
    2. 选择2
    3. Choice3
    4. ?:(在此输入输入)

      此解决方案仍然不允许用户返回上一个菜单或退出。我正在考虑制作一个菜单类来处理菜单,但我仍然不太喜欢面向对象的Perl。这是一个只有少量菜单的小程序,因此使用复杂的菜单构建模块可能有点过分。我想尽可能保持我的代码。

      编辑:

      感谢您的快速回复!但是仍然存在问题。当我从&#34; Menu1&#34;中选择一个选项时然后它进展到&#34; Menu2&#34;我希望保存选择来自&#34; Menu1&#34;供以后使用:

      菜单1:

      1. 选择1&lt; - 存储值(如果已选择)并转到下一个菜单
      2. Choice2&lt; - ...
      3. 退出&lt; - 退出
      4. 菜单2:

        1. 选择1&lt; - 存储值(如果已选择)并转到下一个菜单
        2. Choice2&lt; - ...
        3. 返回&lt; - 返回上一级菜单以重新选择值
        4. 退出&lt; - 退出
        5. 选择Choice1或Choice2应该在变量中存储一个值,以便以后使用 进入下一个菜单。然后,如果您选择从Menu2返回第一个菜单,它将为您提供重新选择并重新定义变量的选项。我试图避免使用全局变量,这使得这非常困难。

          在浏览完所有菜单并设置所有这些变量的值之后,我想运行一个子程序来处理所有选项并打印最终输出。

           sub main () {
          
             # DO MENU STUFF HERE
          
             # PROCESS RESULTS FROM MENU CHOICES
             my $output = process($menu1_choice, $menu2_choice, $menu3_choice, ... );
           }
          

          此外,如果有人使用类或其他数据结构使用面向对象的方法,虽然它可能有点矫枉过正,但我​​仍然希望看到它并尝试绕过这个想法!

6 个答案:

答案 0 :(得分:5)

您可以使用Term::Choose

等模块
use Term::Choose qw( choose );

my $submenus = {
    menu1 => [ qw( s_1 s_2 s_3 ) ],
    menu2 => [ qw( s_4 s_5 s_6 s_7) ],
    menu3 => [ qw( s_8 s_9 ) ],
};
my @menus = ( qw( menu1 menu2 menu3 ) );
my $mm = 0;
MAIN: while ( 1 ) {
    my $i = choose( 
        [ undef, @menus ],
        { layout => 3, undef => 'quit', index => 1, default => $mm }
    );
    last if ! $i;
    if ( $mm == $i ) {
        $mm = 0;
        next MAIN;
    }
    else {
        $mm = $i;
    }
    $i--;
    SUB: while ( 1 ) {
        my $choice = choose(
            [ undef, @{$submenus->{$menus[$i]}} ],
            { layout => 3, undef => 'back' }
        );
        last SUB if ! defined $choice;
        say "choice: $choice";
    }
}

答案 1 :(得分:3)

如果你不想用这个来完整的OO,你可以通过一个简单的方法让每个菜单选择控制它的执行方式。让我们说每个菜单都有一个包含菜单文本的哈希数组和一个实现菜单功能的coderef。

use strict;
use warnings;

sub menu {
    my @items = @_;

    my $count = 0;
    foreach my $item( @items ) {
        printf "%d: %s\n", ++$count, $item->{text};
    }

    print "\n?: ";

    while( my $line = <STDIN> ) {
        chomp $line;
        if ( $line =~ m/\d+/ && $line <= @items ) {
            return $items[ $line - 1 ]{code}->();
        }

        print "\nInvalid input\n\n?: ";
    }
}

my @menu_choices;
my @other_menu_choices;

@menu_choices = (
    { text  => 'do something',
      code  => sub { print "I did something!\n" } },
    { text  => 'do something else',
      code  => sub { print "foobar!\n" } },
    { text  => 'go to other menu',
      code  => sub { menu( @other_menu_choices ) } }
);

@other_menu_choices = (
    { text  => 'go back',
      code  => sub { menu( @menu_choices ) } }
);

menu( @menu_choices );

menu子例程采用一系列选项,每个选项&#34;知道&#34;如何执行自己的行动。如果您想在菜单之间切换,菜单选项只需再次使用不同的选项列表调用menu,例如&#34;返回&#34;来自@other_menu_choices的示例。这使得菜单之间的链接非常简单,并且还可以轻松添加退出选项等。

为了保持此代码的清晰和可读性,除了简单的菜单操作之外的任何其他操作,请使用对子例程的命名引用,而不是匿名子例程引用。例如:

@another_menu_options = (
    { text => 'complicated action'
      code => \&do_complicated_action
    }
);

sub do_complicated_action { 
    ...
}

答案 2 :(得分:2)

经过几个月的Perl编程后,我学到了更多关于如何处理对象的知识,并根据friedo的答案编写了一个简单的面向对象的菜单构建模块。

# Menu.pm

#!/usr/bin/perl

package Menu;

use strict;
use warnings;

# Menu constructor
sub new {

    # Unpack input arguments
    my $class = shift;
    my (%args) = @_;
    my $title       = $args{title};
    my $choices_ref = $args{choices};
    my $noexit      = $args{noexit};

    # Bless the menu object
    my $self = bless {
        title   => $title,
        choices => $choices_ref,
        noexit  => $noexit,
    }, $class;

    return $self;
}

# Print the menu
sub print {

    # Unpack input arguments
    my $self = shift;
    my $title   =   $self->{title  };
    my @choices = @{$self->{choices}};
    my $noexit  =   $self->{noexit };

    # Print menu
    for (;;) {

        # Clear the screen
        system 'cls';

        # Print menu title
        print "========================================\n";
        print "    $title\n";
        print "========================================\n";

        # Print menu options
        my $counter = 0;
        for my $choice(@choices) {
            printf "%2d. %s\n", ++$counter, $choice->{text};
        }
        printf "%2d. %s\n", '0', 'Exit' unless $noexit;

        print "\n?: ";

        # Get user input
        chomp (my $input = <STDIN>);

        print "\n";

        # Process input
        if ($input =~ m/\d+/ && $input >= 1 && $input <= $counter) {
            return $choices[$input - 1]{code}->();
        } elsif ($input =~ m/\d+/ && !$input && !$noexit) {
            print "Exiting . . .\n";
            exit 0;
        } else {
            print "Invalid input.\n\n";
            system 'pause';
        }
    }
}

1;

使用此模块,您可以相对轻松地构建菜单并将它们链接在一起。请参阅以下用法示例:

# test.pl

#!/usr/bin/perl

use strict;
use warnings;

use Menu;

my $menu1;
my $menu2;

# define menu1 choices
my @menu1_choices = (
    { text => 'Choice1',
      code => sub { print "I did something!\n"; }},
    { text => 'Choice2',
      code => sub { print "I did something else!\n"; }},
    { text => 'Go to Menu2',
      code => sub { $menu2->print(); }},
);

# define menu2 choices
my @menu2_choices = (
    { text => 'Choice1',
      code => sub { print "I did something in menu 2!\n"; }},
    { text => 'Choice2',
      code => sub { print "I did something else in menu 2!\n"; }},
    { text => 'Go to Menu1',
      code => sub { $menu1->print(); }},
);

# Build menu1
$menu1 = Menu->new(
    title   => 'Menu1',
    choices => \@menu1_choices,
);

# Build menu2
$menu2 = Menu->new(
    title   => 'Menu2',
    choices => \@menu2_choices,
    noexit  => 1,
);

# Print menu1
$menu1->print();

此代码将创建一个带子菜单的简单菜单。进入子菜单后,您可以轻松返回上一个菜单。

感谢所有出色的答案!他们真的帮我解决了这个问题,如果没有所有的帮助,我认为我不会得到如此好的解决方案!


更好的解决方案:

告别那些丑陋的哈希阵列!

Menu.pm和Item.pm模块内部的一些代码可能看起来有点令人困惑,但这种新设计使得构建菜单的界面更加清晰和高效。

经过一些仔细的代码重新编写并将各个菜单项放入自己的对象后,我能够创建一个更清晰的界面来创建菜单。这是我的新代码:

这是一个测试脚本,显示了如何使用模块构建菜单的示例。

# test.pl

#!/usr/bin/perl

# Always use these
use strict;
use warnings;

# Other use statements
use Menu;

# Create a menu object
my $menu = Menu->new();

# Add a menu item
$menu->add(
    'Test'  => sub { print "This is a test\n";  system 'pause'; },
    'Test2' => sub { print "This is a test2\n"; system 'pause'; },
    'Test3' => sub { print "This is a test3\n"; system 'pause'; },
);

# Allow the user to exit directly from the menu
$menu->exit(1);

# Disable a menu item
$menu->disable('Test2');
$menu->print();

# Do not allow the user to exit directly from the menu
$menu->exit(0);

# Enable a menu item
$menu->enable('Test2');
$menu->print();

Menu.pm模块用于构建菜单对象。这些菜单对象可以包含多个Menu :: Item对象。对象存储在一个数组中,因此保留了它们的顺序。

# Menu.pm

#!/usr/bin/perl

package Menu;

# Always use these
use strict;
use warnings;

# Other use statements
use Carp;
use Menu::Item;

# Menu constructor
sub new {

    # Unpack input arguments
    my ($class, $title) = @_;

    # Define a default title
    if (!defined $title) {
        $title = 'MENU';
    }

    # Bless the Menu object
    my $self = bless {
        _title => $title,
        _items => [],
        _exit  => 0,
    }, $class;

    return $self;
}

# Title accessor method
sub title {
    my ($self, $title) = @_;
    $self->{_title} = $title if defined $title;
    return $self->{_title};
}

# Items accessor method
sub items {
    my ($self, $items) = @_;
    $self->{_items} = $items if defined $items;
    return $self->{_items};
}

# Exit accessor method
sub exit {
    my ($self, $exit) = @_;
    $self->{_exit} = $exit if defined $exit;
    return $self->{_exit};
}

# Add item(s) to the menu
sub add {

    # Unpack input arguments
    my ($self, @add) = @_;
    croak 'add() requires name-action pairs' unless @add % 2 == 0;

    # Add new items
    while (@add) {
        my ($name, $action) = splice @add, 0, 2;

        # If the item already exists, remove it
        for my $index(0 .. $#{$self->{_items}}) {
            if ($name eq $self->{_items}->[$index]->name()) {
                splice @{$self->{_items}}, $index, 1;
            }
        }

        # Add the item to the end of the menu
        my $item = Menu::Item->new($name, $action);
        push @{$self->{_items}}, $item;
    }

    return 0;
}

# Remove item(s) from the menu
sub remove {

    # Unpack input arguments
    my ($self, @remove) = @_;

    # Remove items
    for my $name(@remove) {

        # If the item exists, remove it
        for my $index(0 .. $#{$self->{_items}}) {
            if ($name eq $self->{_items}->[$index]->name()) {
                splice @{$self->{_items}}, $index, 1;
            }
        }
    }

    return 0;
}

# Disable item(s)
sub disable {

    # Unpack input arguments
    my ($self, @disable) = @_;

    # Disable items
    for my $name(@disable) {

        # If the item exists, disable it
        for my $index(0 .. $#{$self->{_items}}) {
            if ($name eq $self->{_items}->[$index]->name()) {
                $self->{_items}->[$index]->active(0);
            }
        }
    }

    return 0;
}

# Enable item(s)
sub enable {

    # Unpack input arguments
    my ($self, @enable) = @_;

    # Disable items
    for my $name(@enable) {

        # If the item exists, enable it
        for my $index(0 .. $#{$self->{_items}}) {
            if ($name eq $self->{_items}->[$index]->name()) {
                $self->{_items}->[$index]->active(1);
            }
        }
    }
}

# Print the menu
sub print {

    # Unpack input arguments
    my ($self) = @_;

    # Print the menu
    for (;;) {
        system 'cls';

        # Print the title
        print "========================================\n";
        print "    $self->{_title}\n";
        print "========================================\n";

        # Print menu items
        for my $index(0 .. $#{$self->{_items}}) {
            my $name   = $self->{_items}->[$index]->name();
            my $active = $self->{_items}->[$index]->active();
            if ($active) {
                printf "%2d. %s\n", $index + 1, $name;
            } else {
                print "\n";
            }
        }
        printf "%2d. %s\n", 0, 'Exit' if $self->{_exit};

        # Get user input
        print "\n?: ";
        chomp (my $input = <STDIN>);

        # Process user input
        if ($input =~ m/^\d+$/ && $input > 0 && $input <= scalar @{$self->{_items}}) {
            my $action = $self->{_items}->[$input - 1]->action();
            my $active = $self->{_items}->[$input - 1]->active();
            if ($active) {
                print "\n";
                return $action->();
            }
        } elsif ($input =~ m/^\d+$/ && $input == 0 && $self->{_exit}) {
            exit 0;
        }

        # Deal with invalid input
        print "\nInvalid input.\n\n";
        system 'pause';
    }
}

1;

Item.pm模块必须存储在名为“Menu”的子文件夹中才能正确引用。此模块允许您创建包含名称和子例程引用的Menu :: Item对象。这些对象将是用户在菜单中选择的对象。

# Item.pm

#!/usr/bin/perl

package Menu::Item;

# Always use these
use strict;
use warnings;

# Menu::Item constructor
sub new {

    # Unpack input arguments
    my ($class, $name, $action) = @_;

    # Bless the Menu::Item object
    my $self = bless {
        _name   => $name,
        _action => $action,
        _active => 1,
    }, $class;

    return $self;
}

# Name accessor method
sub name {
    my ($self, $name) = @_;
    $self->{_name} = $name if defined $name;
    return $self->{_name};
}

# Action accessor method
sub action {
    my ($self, $action) = @_;
    $self->{_action} = $action if defined $action;
    return $self->{_action};
}

# Active accessor method
sub active {
    my ($self, $active) = @_;
    $self->{_active} = $active if defined $active;
    return $self->{_active};
}

1;

这种设计比我以前的设计有了很大的改进,使创建菜单变得更加容易和清洁。

让我知道你的想法。

任何评论,想法或改进意见?

答案 3 :(得分:1)

以下是一种方法。每个选项都有一个相关的子程序。选择后,调用相应的子程序。这里我使用匿名子例程,但您也可以使用对命名子例程的引用。

use warnings; use strict;

sub menu {
  my $args = shift;
  my $title = $args->{title};
  my $choices = $args->{choices};

  while (1) {
    print "--------------------\n";
    print "$title\n";
    print "--------------------\n";
    for (my $i = 1; $i <= scalar(@$choices); $i++) {
      my $itemHeading = $choices->[$i-1][0];
      print "$i.\t $itemHeading\n";
    }
    print "\n?: ";
    my $i = <STDIN>; chomp $i;
    if ($i && $i =~ m/[0-9]+/ && $i <= scalar(@$choices)) {
      &{$choices->[$i-1][1]}();
    } else {
      print "\nInvalid input.\n\n";
    }
  }
}

my $menus = {};
$menus = {
  "1" => {
    "title" => "Menu 1 header",
    "choices" => [
       [ "Choice 1" , sub { print "Choice 1 selected"; }],
       [ "Choice 2" , sub { print "Choice 2 selected"; }],
       [ "Menu 2" , sub { menu($menus->{2}); }],
       [ "Exit" , sub { exit; }],
    ],
  },
 "2" => {
    "title" => "Menu 2 header",
    "choices" => [
       [ "Choice 3" , sub { print "Choice 3 selected"; }],
       [ "Choice 4" , sub { print "Choice 4 selected"; }],
       [ "Menu 1" , sub { menu($menus->{1}); }],
       [ "Exit" , sub { exit; }],
  ],
  },
};

menu($menus->{1});

答案 4 :(得分:1)

感谢大家的回复!所有这三个回复都有助于最终提出我的解决方案。我决定使用Term :: Choose模块,(感谢sid_com的想法)。我的菜单结构与你最初的建议不同,我花了很长时间才弄清楚如何让它完全符合我的想法。希望这个解决方案可以帮助遇到类似问题的其他人。

我构建了如下所示的菜单:

我用更通用的名称替换了我的变量,因此更容易理解

    #!/usr/bin/perl

    use strict;
    use warnings;
    use Term::Choose qw(choose);

    my @CHOICES1 = ('A','B','C');
    my @CHOICES2 = ('1','2','3');
    my @CHOICES3 = ('BLUE','YELLOW','GREEN');

    # function to use the choices
    sub some_function {
        print "THIS IS SOME FUNCTION!\n";
        print "Choice 1 is $_[0]\n";
        print "Choice 2 is $_[1]\n";
        print "Choice 3 is $_[2]\n";
        print "Have a nice day! :)\n";
    }

    sub main() {

        # clear the screen
        # (for some reason the build in screen clear 
        # for the module was not working for me)
        system ('cls');

        # create menu object
        my $menu = new Term::Choose();

        # menu 1
        for (;;) {
            my $choice1 = $menu->choose(
                [@CHOICES1, undef],
                {
                    prompt => 'Select a choice1:',
                    undef  => 'Exit',
                    layout => 3,
                }
            );
            last if ! $choice1;

            # submenu 1
            for (;;) {
                my $choice2 = $menu->choose(
                    [@CHOICES2, undef],
                    {
                        prompt => 'Select a choice2:',
                        undef  => 'Back',
                        layout => 3,
                    }
                );
                last if ! $choice2;

                # submenu2
                for (;;) {
                    my $choice3 = $menu->choose(
                        [@CHOICES3, undef],
                        {
                             prompt => 'Select a choice3:',
                            undef  => 'Back',
                            layout => 3,
                        }
                    );
                    last if ! $choice3;

                    # function operating on all choices
                    some_function($choice1, $choice2, $choice3);
                    return;
                }
            }
        }
    }

    main();

对于面向对象的Perl来说,我还是很陌生,所以这花了很长时间才弄清楚它可能并不完美,但它完成了工作。如果您有任何想法或改进,请告诉我们!

答案 5 :(得分:-1)

我在perl模块中找到了这个没有任何perldoc的旧模块...... 请试一试......

#!/usr/bin/perl
    BEGIN { $Curses::OldCurses = 1; }
    use Curses;
    use perlmenu;
    &menu_init(0,"Select an Animal"); # Init menu

    &menu_item("Collie","dog"); # Add item
    &menu_item("Shetland","pony"); # Add item
    &menu_item("Persian","cat"); # Add last item

    $sel = &menu_display("Which animal?"); # Get user selection

    if ($sel eq "dog") {print "Its Lassie!\n";}