65.9K
CodeProject 正在变化。 阅读更多。
Home

动态菜单创建

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.57/5 (21投票s)

2002年9月22日

6分钟阅读

viewsIcon

221187

downloadIcon

2822

动态创建菜单,其结构定义在Access数据库中。

引言 - Marc Clifton

好吧,我是一个无政府主义者。我不喜欢用“窗体设计器”来设计用户界面。我认为它不利于重用,而且我认为(尤其是在MFC中),它会助长非常糟糕的代码实践——实际上是通用的功能却被绑定到特定的GUI类上。(你们这些“优秀”的程序员肯定不会这么做,对吧?)

我也认为好的代码应该看起来很好。坦率地说,VS.NET 生成菜单代码的方式非常难看。简单粗暴地实例化MenuItem对象和集合没有任何优雅之处。

你可能不同意我的无政府主义想法,认为它们过时,甚至更糟,你可能认为我的代码更难看。随它去吧。这里有一套库,可以根据Access数据库中定义的结构来生成窗体的菜单结构。

窗体创建

第一步是创建窗体并为其关联菜单。剥离所有.NET的开销,我们可以轻松创建一个窗体。
  1. 我们需要的一些东西
  2. using System;
    using System.Windows.Forms;
    
    using KA;
    using KA.DataContainer;
    using KA.DatabaseIO;
    using KA.DataMatrix;
    using KA.DBG;
    using KA.EventManager;
    using KA.MenuManager;
    
  3. 必须创建窗体类
    public class Form : System.Windows.Forms.Form
    {
    }
    
  4. 我们定义一个包含静态 `Main` 方法的类
    class AppMain
    {
        static void Main()
        {
            app=new App();
            app.Init();
        }
        static public App app;
    }
    
  5. 我们定义 `App` 类
    public void Init()
    {
        KA.DBG.Trace.Initialize("menuDemoTrace.txt");    
        sb=new StatusBar();
        sb.Text="Ready";
        Form form=new Form();
        form.Controls.Add(sb);
        menu=new KA.MenuManager.Menu();
        menu.helpText=new EventHandler(SetMenuHelp);
        menu.SetFormMenu(form, ".\\menu.mdb", "MainMenu");
        KA.EventManager.EventCollection.Add("ExitApp", new EventHandler(ExitApp));
        KA.EventManager.EventCollection.Add("About", new EventHandler(About));
        KA.EventManager.EventCollection.Add("CheckMe", new EventHandler(CheckMe));
        KA.EventManager.EventCollection.Add("RadioMe", new EventHandler(RadioMe));
        Application.Run(form);
        KA.DBG.Trace.Terminate();
    }
    
  6. 我们声明需要的变量
    private StatusBar sb=null;
    private KA.MenuManager.Menu menu=null;
    
第4步中的代码做什么?
KA.DBG.Trace.Initialize("menuDemoTrace.txt");
这会初始化我的跟踪调试器。抽象事件管理的一个好处是所有事件都可以记录在跟踪文件中。SQL语句也一样,因为所有数据库函数都有一个单一的接口类。这使得我们的应用程序代码看起来更整洁,因为它没有被日志记录函数(好像任何人的代码实际上有一样!)弄得乱七八糟。

接下来的几行实例化了状态栏和窗体,用于显示菜单项的上下文帮助。菜单系统会回调到应用程序定义的事件来实际显示,所以应用程序可以确定菜单帮助文本在哪里以及如何显示。这在下面的代码中完成,在菜单实例化之后。
menu=new KA.MenuManager.Menu();
menu.helpText=new EventHandler(SetMenuHelp);
接下来,通过指定窗体、包含菜单结构的数据库以及根目录或主菜单集合,将菜单与窗体关联起来。
menu.SetFormMenu(form, ".\\menu.mdb", "MainMenu");

菜单事件关联

接下来,事件处理程序与菜单事件关联。事件名称在数据库中声明,应用程序代码必须匹配这些名称,以便菜单管理器找到相应菜单项的处理程序。
KA.EventManager.EventCollection.Add("ExitApp", new EventHandler(ExitApp));
KA.EventManager.EventCollection.Add("About", new EventHandler(About));
KA.EventManager.EventCollection.Add("CheckMe", new EventHandler(CheckMe));
KA.EventManager.EventCollection.Add("RadioMe", new EventHandler(RadioMe));
启动过程的其余部分是启动窗体,并在终止时关闭调试跟踪器。

事件处理程序

这些事件处理程序演示了系统的有效性以及一些简单的菜单项操作。
private void ExitApp(object obj, EventArgs e)
{
    Application.Exit();
}

private void About(object obj, EventArgs e)
{
    MessageBox.Show("Dynamic menu creation demo.\nV 0.10", "About"); 
}

private void CheckMe(object obj, EventArgs e)
{
    menu["ViewChecked"].Checked^=true;
}

private void RadioMe(object obj, EventArgs e)
{
    menu["ViewRadio"].Checked^=true;
}

菜单管理器

菜单管理器从数据库读取结构,然后在.Net框架中实例化菜单对象,最后将它们与窗体关联。完成此操作的主要方法是 `SetFormMenu`。
public void SetFormMenu(System.Windows.Forms.Form form, string dbName, string menuName)
{
    dc=new DC();
    dbio=new DBIO(dc);
    dbio.SetFileName(dbName);
    dbio.Open();
    sql="SELECT b.CAPTION, b.HELP, b.ENABLED, b.IS_CHECKED, " +
        "b.IS_RADIO, b.SHORTCUT_KEY, b.IS_VISIBLE, b.CLICK_EVENT, " +
         "b.SELECT_EVENT, b.POPUP_MENU, b.NAME FROM MENU_ITEM_COLLECTION AS a," +
         "MENU_ITEM AS b WHERE b.NAME=a.ITEM_NAME and " +
         "a.MENU_NAME='{menuName}' ORDER BY a.SEQ";
    MainMenu mainMenu=new MainMenu();
    LoadMenu(mainMenu, menuName);
    dbio.Close();
    form.Menu=mainMenu;
    form.MenuComplete+=new EventHandler(Complete);
}
此函数实例化一个数据容器和数据库IO对象。对于菜单结构的顶层和子层,SQL语句是相同的,因此只需要一个SQL语句。接下来,函数实例化 `MainMenu` .NET对象并调用我们的 `LoadMenu` 方法,然后进行清理。

主菜单项在 `MainMenu` 对象中实例化,而子菜单项在 `MenuItem` 对象中实例化。我没这么做!.NET就是这样做的,因此,我们的菜单加载器必须有两种不同的加载机制,一种用于主菜单项,一种用于子菜单项。

另外,请注意 `MenuComplete` 事件的定义。感谢James T. Johnson为我找到它!必须定义此事件,以便在用户单击菜单项或单击菜单外部以取消菜单操作后,状态栏可以恢复为应用程序的默认值。MFC处理起来要容易得多,也好得多。好吧。

加载主菜单项

private void LoadMenu(MainMenu mainMenu, string menuName)
{
    dc.Set("menuName", menuName);
    DM menuList=dbio.QueryMultiRow(sql, "menuList");
    for (int i=0; i<menuList.GetRows(); i++)
    {
        KAMenuItem menuItem=new KAMenuItem(menuList.GetCell(0, i),
                                            "", "", "");
        mainMenu.MenuItems.Add(menuItem);
        menuItemList[menuList.GetCell(10, i)]=menuItem;
        string subMenu=menuList.GetCell(9, i);
        if (subMenu != "")
        {
            LoadMenu(menuItem, subMenu);
        }
    }
}
上面的代码在数据容器中声明了菜单集合名称 `menuName`,它由数据库IO管理器中的SQL预解析器(本文未讨论——其中很多仍在开发中)提取。查询数据库,然后迭代结果行,创建顶层菜单项。如果为菜单项定义了子菜单,则递归调用 `LoadMenu` 方法。但是什么!这是一个不同的 `LoadMenu` 方法,用于加载子菜单及其子子菜单等。

加载子菜单项

private void LoadMenu(MenuItem menuItem, string menuName)
{
    dc.Set("menuName", menuName);
    DM menuList=dbio.QueryMultiRow(sql, "menuList");
    for (int i=0; i<menuList.GetRows(); i++)
    {
        KAMenuItem menuItem2=new KAMenuItem(menuList.GetCell(0, i), 
                 menuList.GetCell(7, i), menuList.GetCell(8, i), 
                 menuList.GetCell(1, i));
        menuItemList[menuList.GetCell(10, i)]=menuItem2;
        menuItem2.Click+=new EventHandler(ClickMenuItem);
        menuItem2.Select+=new EventHandler(SelectMenuItem);
        if (sortedShortcutMap.Contains(menuList.GetCell(5, i)))
        {
            menuItem2.Shortcut=
                (Shortcut)sortedShortcutMap[menuList.GetCell(5, i)];
        }

        if (menuList.GetCellInt(2, i) != 1)
        {
            menuItem2.Enabled=false;
        }

        if (menuList.GetCellInt(3, i) == 1)
        {
            menuItem2.Checked=true;
        }

        if (menuList.GetCellInt(4, i) == 1)
        {
            menuItem2.RadioCheck=true;
        }

        menuItem.MenuItems.Add(menuItem2);
        string subMenu=menuList.GetCell(9, i);
        if (subMenu != "")
        {
            LoadMenu(menuItem2, subMenu);
        }
    }
}
此方法与主菜单加载器非常相似。它另外执行设置各种菜单项状态信息的工作。它还将 Click 和 Select 事件设置为菜单管理器内部的事件。此外,它根据数据库定义中为菜单项提供的文本来定义菜单快捷键。这真的很烦人,因为我不得不将 `Shortcut` 枚举映射到文本消息。在MFC中这样做要容易得多,在那里你可以定义虚拟键!

Select 事件

每当鼠标悬停在菜单项上时,我们都希望有机会调用应用程序定义的 Select 事件,并在状态栏上显示该项的帮助文本。这可以通过以下事件处理程序来完成。
private void SelectMenuItem(object obj, EventArgs e)
{
    KAMenuItem menuItem=(KAMenuItem)obj;
    EventCollection.Invoke(obj, menuItem.selectEvent, "");
    if (helpText != null)
    {
        helpText(menuItem, new MenuArgs(menuItem.help));
    }
}
此代码调用任何应用程序定义的事件处理程序,并且如果实例化了帮助文本事件处理程序,它也会调用它。 `helpText` 事件处理程序由应用程序定义(如上所述)。

Click 事件

此事件调用用户实际单击菜单项时的处理程序。
private void ClickMenuItem(object obj, EventArgs e)
{
    KAMenuItem menuItem=(KAMenuItem)obj;
    EventCollection.Invoke(obj, menuItem.clickEvent, "");
}

Complete 事件

当用户单击菜单项或单击菜单外部导致菜单失去焦点并关闭时,将调用此事件。处理此事件为应用程序提供了将状态栏文本恢复为其默认值 Thus 的机会。
private void Complete(object obj, EventArgs e)
{
    if (helpText != null)
    {
        helpText(obj, new MenuArgs(""));
    }
}

这个 EventCollection 是什么?

`EventCollection` 是我将系统事件(如菜单事件)与应用程序特定处理程序之间的抽象层。你们这些设计模式的倡导者对这个概念有各种各样的称呼。对此类的进一步讨论超出了本文的范围,并且请注意,此类仍在开发中!

那么这个 KA 命名空间是什么?

嗯,KA 代表 Knowledge Automation,这是我的公司名称!

结论

好了,就这些了!源代码包含了所有项目文件和一个示例数据库。数据库架构非常简单,你应该能够自己弄清楚。现在我只需要有人写一个菜单设计器,将信息加载到数据库中(哈哈哈哈)。

更新 - Martin Robins

虽然上面的代码确实完成了工作,但它依赖于基础库,并且需要数据库访问例程来填充菜单。根据下面的评论,我决定尝试做得更好。

该代码提供了一个基础窗体,该窗体从XML文件(已包含)加载菜单项,显示XML文件中指定的窗体,或者在XML引用找不到的内容时显示错误。

它不漂亮;事实上,它非常直观——但它完成了工作!

© . All rights reserved.