动态菜单创建






3.57/5 (21投票s)
2002年9月22日
6分钟阅读

221187

2822
动态创建菜单,其结构定义在Access数据库中。
引言 - Marc Clifton
好吧,我是一个无政府主义者。我不喜欢用“窗体设计器”来设计用户界面。我认为它不利于重用,而且我认为(尤其是在MFC中),它会助长非常糟糕的代码实践——实际上是通用的功能却被绑定到特定的GUI类上。(你们这些“优秀”的程序员肯定不会这么做,对吧?)我也认为好的代码应该看起来很好。坦率地说,VS.NET 生成菜单代码的方式非常难看。简单粗暴地实例化MenuItem对象和集合没有任何优雅之处。
你可能不同意我的无政府主义想法,认为它们过时,甚至更糟,你可能认为我的代码更难看。随它去吧。这里有一套库,可以根据Access数据库中定义的结构来生成窗体的菜单结构。
窗体创建
第一步是创建窗体并为其关联菜单。剥离所有.NET的开销,我们可以轻松创建一个窗体。- 我们需要的一些东西
- 必须创建窗体类
public class Form : System.Windows.Forms.Form { }
- 我们定义一个包含静态 `Main` 方法的类
class AppMain { static void Main() { app=new App(); app.Init(); } static public App app; }
- 我们定义 `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(); }
- 我们声明需要的变量
private StatusBar sb=null; private KA.MenuManager.Menu menu=null;
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;
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引用找不到的内容时显示错误。
它不漂亮;事实上,它非常直观——但它完成了工作!