基于文本的菜单

一个简单的基于文本的菜单系统

读这个

这个职位 ; 有很多这样的帖子:一个新手在某种菜单实现上苦苦挣扎。 他们当然想要嵌套菜单,并希望有一个选项可以退出整个过程。 当然,他们将自己的“业务规则”与他们对单个菜单项的(较弱的)尝试紧密地结合在一起,并陷入了复杂的控制结构中。

帖子(请参阅上面的链接)使我写了这篇小文章。 当然,这很麻烦,因为本文中的代码可以随意使用,并且可以避免所有新程序员再次发明轮子,因为我在本文中只是这样做。 这里的天气晴朗,我坐在我的后花园里,现在是星期天,所以我要休息几个小时。 开始:

该设计

我想实现一个简单的文本菜单系统; 菜单可以是另一个菜单(或更多菜单)的一部分。 选择一个选项(“菜单项”)或者打开另一个菜单,或者激活一些代码。 该代码完成后,将再次显示相同的菜单。 用户必须能够再次“向上”导航,即包含当前菜单作为项目的菜单将再次显示。 用户还必须能够退出整个过程。

当您想到它时:当前菜单是顶部菜单时,向上导航没有意义; 我们将使导航成为可选。 同样,我们也不希望用户能够退出每个菜单中的所有内容。 我们还将使它成为可选功能。

因为这是一个基于文本的简单菜单系统,所以我们将在控制台上以简单的字符串形式显示选项,并在其前面加上如下数字:

菜单标题:

0:第一选择

1:第二种选择

2:第三个选择

...

然后,我们提示用户键入0、1、2 ...最多n-1,其中菜单中有n个选项。 当然,不正确的输入值得警告并重试。 “退出”和“返回”选项将是任何菜单中的最后一个选项(如果存在)。

一个TextMenuItem

菜单项是什么意思? 它由单行文本和选择该项目时必须执行的一段代码表示。 这很容易; 这是TextMenuItem类:


package textmenu; 
public class TextMenuItem implements Runnable { 
    private String title;
    private Runnable exec; 
    protected TextMenuItem(String title) { this(title, null); } 
    public TextMenuItem(String title, Runnable exec) {
        this.title= title;
        this.exec= exec;
    } 
    public String getTitle() { return title; } 
    public boolean isExec() { return exec != null; } 
    protected void setExec(Runnable exec) { this.exec= exec; } 
    public void run() { 
        try {
            exec.run();
        }
        catch (Throwable t) {
            t.printStackTrace(System.err);
        }
    }
} 
此类的构造方法具有String标题和选择该项目时要执行的可选代码段。 当然,这段代码可能会抛出某种Exception,我们将捕获该异常并显示Exception堆栈跟踪。

我决定将该代码段建模为Runnable。 有关该接口的所有详细信息,请参见API文档。 您可以在Java论坛的“答案”部分的第一篇文章中找到该文档的链接。 强烈建议您下载文档,并将其保存在本地硬盘上的某个地方,您会经常需要它。

回到该类:它的标题带有一个“ getter”,可执行代码段(“ Runnable”)带有一个“ setter”。 我们还可以检查是否以前通过'isExec()'方法设置了此类代码。

TextMenuItem类本身也实现了Runnable接口,并因此实现了“ run()”方法。 它所做的只是从可执行代码段中盲目调用'run()'方法,并捕获可能抛出的任何异常(Throwables)。

但是,如果exec成员变量为null怎么办? 谁或什么负责? 想一想:TextMenuItem只能从菜单中选择; 没有孤立的TextMenuItems。 TextMenu会解决这个问题。 按照约定,只有“后退” TextMenuItem的exec成员变量为空,所有其他TextMenuItems的非exec成员变量必须为非空。 这就是第一个构造函数之所以不公开的原因之一,因为我不希望任何人调用它并破坏游戏。

如果仔细看一下第一个构造函数,您会发现它是受保护的构造函数。 这意味着只能从类本身及其子类中调用它。 其他人(用户)必须调用第二个构造函数。

菜单在另一个菜单中也将其自身显示为一行文本(当然,除非它是顶级菜单)。 所以以某种方式TextMenu

TextMenuItem,可以是该第一类的子类。

选择TextMenu时,它的任务是什么? 基本上,它必须显示其所有的TextMenuItem,提示用户选择其中之一并执行该TextMenuItem。 TextMenu所需要做的就是调用其“ run()”方法; 有一个例外:如果TextMenuItem的'isExec()'方法被调用时,返回false,则选择'back'选项,并且当前运行的TextMenu必须从其自己的'run()'方法返回。 考虑一下,这将是有道理的。

一个文本菜单

首先,我将展示该类(它比第一个类大很多,但是它做得更多),然后我们将剖析所有代码; 这里是:


package textmenu; 
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; 
public class TextMenu extends TextMenuItem { 
    private static final TextMenuItem quit= new TextMenuItem("quit", new Runnable() {
        public void run() {
            System.exit(0);
        }
    }); 
    private static final TextMenuItem back= new TextMenuItem("back"); 
    List<TextMenuItem> items; 
    public TextMenu(String title, TextMenuItem ... items) { this(title, false, true, items); } 
    public TextMenu(String title, boolean addBack, boolean addQuit, TextMenuItem ... items) {
        super(title);
        setExec(this); 
        initialize(addBack, addQuit, items);
    } 
    private void initialize(boolean addBack, boolean addQuit, TextMenuItem ... items) { 
        this.items= new ArrayList<TextMenuItem>(Arrays.asList(items));
        if (addBack) this.items.add(back);
        if (addQuit) this.items.add(quit);
    } 
    private void display() { 
        int option= 0;
        System.out.println(getTitle()+":");
        for (TextMenuItem item : items) {
            System.out.println((option++)+": "+item.getTitle());
        }
        System.out.print("select option: ");
        System.out.flush();
    } 
    private TextMenuItem prompt() throws IOException { 
        BufferedReader br= new BufferedReader(new InputStreamReader(System.in)); 
        while (true) { 
            display(); 
            String line = br.readLine();
            try {
                int option= Integer.parseInt(line);
                if (option >= 0 && option < items.size())
                    return items.get(option);
            }
            catch (NumberFormatException e) { } 
            System.out.println("not a valid menu option: "+line);
        } 
    } 
    public void run() { 
        try {
            for (TextMenuItem item= prompt(); item.isExec(); item= prompt())
                item.run();
        }
        catch (Throwable t) {
            t.printStackTrace(System.out);
        }
    }
} 
该类与第一个存储在相同的程序包中,并且从该程序包扩展而来,因此它是一个 TextMenuItem。 更重要的是,它可以从其超类中调用第一个受保护的构造函数。 它必须能够做到这一点,因为Java语言的特殊性使我们无法采取其他措施。 我们稍后再讨论。

此类的前几行定义了两个私有静态成员:“后退”菜单项和“退出”菜单项。 我们不希望用户显式地传递这些参数,因此我们在此处定义它们并让TextMenu完成。

这两个构造函数都使用String标题和TextMenuItems列表来填充当前菜单。 第二个构造函数还接受两个布尔参数,告诉它是否添加“后退”菜单项和/或“退出”菜单项。

如果您查看第二个构造函数,它将调用其超类的受保护构造函数,将其自身设置为一段可执行代码并进行初始化。 为什么不按如下所示调用另一个超类构造函数:


super(title, this); 
毕竟,它表示与菜单关联的Runnable代码。 当构造函数调用其超类构造函数时,还没有“ this”。 Java禁止使用这些奇特的东西(出于这种原因,这是本文所不具备的)。

因此,构造函数通过调用其受保护的超类构造函数仅设置标题,并在第二步通过在其超类中调用受保护的“ setExec()”方法将自身设置为可运行的代码段。 类层次结构之外的任何事情都不能做到这一点,我们希望做到这一点。 这样一来,Java的独特性就变成了一件好事……

私有的“ initialize()”方法将构建整个菜单:它将创建所有TextMenuItems的副本的列表,并可以选择添加“后退”和“退出”菜单项。 为什么要创建一个已经是所有TextMenutems副本的列表副本? 第一个列表是“直写”列表(请阅读API文档); 这意味着,如果这些类之外的人更改了数组中的某些内容,这些更改将在列表中显示出来。 我们不想要那样,所以我们复制了第一个列表。 第一个列表也是固定大小的列表,如果以后要向菜单中添加其他TextMenuItem,则将无法执行此操作。

从另一个菜单中选择此菜单会怎样? 它的'run()'方法将被调用; 它基本上是这样做的:


for (TextMenuItem item= prompt(); item.isExec(); item= prompt())
    item.run(); 
在这里,您需要了解'for'语句的工作原理:首先调用'prompt()'方法,该方法返回选定的TextMenuItem; 如果该项目不可执行,则退出循环。 它是该项目具有其'run()'方法的调用,最后再次提示用户。

在原始方法主体中,您可以看到如果出现问题(引发Exception),则该方法将捕获该错误,将其显示并终止当前菜单的执行。 除了显示出问题之外,还应该做什么? 在功能更强的菜单系统中,可能会实现更巧妙的异常处理,但我们现在想使其更简单。

我们正在研究菜单处理的细节。 'prompt()'方法的主体如下所示:


BufferedReader br= new BufferedReader(new InputStreamReader(System.in)); 
while (true) { 
    display(); 
    String line = br.readLine();
    try {
        int option= Integer.parseInt(line);
        if (option >= 0 && option < items.size())
            return items.get(option);
    }
    catch (NumberFormatException e) { } 
    System.out.println("not a valid menu option: "+line); 
首先,它为System.in流设置了BufferedReader,因为我们想从(控制台)输入中读取整行。 我们希望用户在0到n-1之间键入一个整数,其中n是“项目”列表的大小。 如果出了什么问题,它会捕获到异常,并输出警告并重复提示。 如果一切顺利,则返回所选的TextMenuItem。 该方法总是在期望用户输入之前显示整个菜单。

请注意,我们仅捕获由于错误输入而引发的异常,而不是因为某些宠物鹦鹉咀嚼了键盘的电线或什至更灾难性的东西。 此类事件很可能引发另一种Exception。 此类异常由调用该方法的'run()'方法处理,它将打印异常堆栈跟踪并终止。

最后要做的是显示整个菜单:显示TexTMenu的标题,然后显示带编号的TextMenuItems列表。 这不是火箭科学,您可以自己弄清楚以下代码的作用:


int option= 0;
System.out.println(getTitle()+":");
for (TextMenuItem item : items) {
    System.out.println((option++)+": "+item.getTitle());
}
System.out.print("select option: ");
System.out.flush(); 
就是这样 我们要做的就是构建一个可以在菜单系统中播放的测试代码。 我们的代码使用两个菜单,一个顶级菜单,如下所示:

顶部菜单:

0:项目1

1:嵌套菜单

2:退出

和这样的嵌套菜单:

嵌套菜单:

0:项目2

1:项目3

2:返回

请注意,顶层菜单有一个“退出”项,但没有“后退”项,而嵌套菜单没有“退出”项,但它确实有一个“后退”项。 这是测试代码; 这有点无聊,但它确实可以做,并且可以稍微锻炼菜单系统:


public class TestTextMenu { 
    private static TextMenuItem item1= new TextMenuItem("item 1",new Runnable() {
        public void run() {
            System.out.println("running item 1");
        }
    }); 
    private static TextMenuItem item2= new TextMenuItem("item 2",new Runnable() {
        public void run() {
            System.out.println("running item 2");
        }
    }); 
    private static TextMenuItem item3= new TextMenuItem("item 3",new Runnable() {
        public void run() {
            System.out.println("running item 3");
        }
    }); 
    private static TextMenu nestedMenu= new TextMenu("nested menu", true, false, item2, item3);
    private static TextMenu topMenu= new TextMenu("top menu", false, true, item1, nestedMenu); 
    public static void main(String[] args) { 
        topMenu.run();
    }
} 
该代码将构建三个TextMenuItems和两个TextMenus。 “ main()”方法将运行顶层菜单。 现在就这样,使用所需的代码即可; 可以通过几种方式对其进行增强:

使用JOptionPanes显示菜单和提示;

有更多高级导航选项(尝试入门的“顶部”项目);

提供帮助文本;

使它的行为类似于JMenus和JMenuItems(阅读API文档);

使“退出”项目更有礼貌;

使您的鹦鹉表现得更明智。

除最后一个选项外,其他所有选项均可行。 玩得开心。

亲切的问候,

乔斯

ps。 如有任何讨论,请在“主题”>“ Java”>“答案”部分中发布它们; 本文已关闭。

From: https://bytes.com/topic/java/insights/870013-text-based-menus

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值