第 5 章 用户互操作:提示和选择
背景
提示通常包含一个描述性信息,伴随一个停止以让用户理解所给的信息并输入数据。数据可以通过多种方式被输入,如通过命令行、对话框或AutoCAD编辑窗口。给出的提示要遵循一定的格式,格式要与一般的AutoCAD提示相一致,这一点是非常重要的。例如,关键字要用“/”号分隔并放在方括号“[]”中,缺省值要放在“<>”内。对于一个AutoCAD用户来说,坚持统一的格式将会减少信息理解错误的产生。
当用户在AutoCAD命令行中选择一个实体时,实体是使用选择机制被选择的。这种机制包括一个提示,用来让用户知道选择什么并怎样选择(如,窗口或单一实体),然后是一个停顿。
试一下诸如PINE这种命令来看一下提示的显示,PEDIT来看一下使用单一实体或多线来进行选择。
练习
Prompts:
提示:
在本章中,我们将提示输入雇员名字、职位、薪水和部门来创建一个雇员块索引对象。如果输入的部门不存在,我们将提示输入部门经理的名字来创建一个新的部门。在我们继续之前,让我们试着重用以前的代码。
为了进行选择,我们将提示用户在一个窗口中进行选择或选择一个实体,而我们只显示选择集中的雇员对象。
在前面的章节中,我们创建了一个名叫“Earnest Shackleton”的雇员,名字被存储为“EmployeeBlock”块定义(块表记录)中的MText。如果我们多次插入这个块,那么我们看到的都是同一个雇员的名字。我们怎样才能自定义这个块以使每次插入这个块的时候显示不同雇员的名字?这就要使用块属性的功能了。属性是存储在每一个块索引实例中的文本,并被作为实例的一部分来被显示。属性从存储在块表记录中的属性定义中继承相关的属性。
属性:
让我们来把MText实体类型改变为属性定义。在CreateEmployeeDefinition()函数中,把下面的代码替换
//文本:
MText text = new MText();
text.Contents = "Earnest Shackleton";
text.Location = center;
为
//属性定义
AttributeDefinition text = new AttributeDefinition(center, "NoName", "Name:", "Enter Name", db.Textstyle);
text.ColorIndex = 2;
试着使用TEST命令来测试一下CreateEmployeeDefinition()函数:
[CommandMethod("Test")]
public void Test()
{
CreateEmployeeDefinition();
}
你现在应该可以使用INSERT命令来插入EmployeeBlock块并对每一个实例确定一个雇员名。
当你插入Employee块时,请注意一下块插入的位置。它是正好被放置在所选点还是有些偏移?试试怎样修复它。(提示:检查块定义中的圆心)
修改CreateEmployee ()以重用
1)让我们来修改CreateEmployee()函数,以让它可以接收名字、薪水、部门和职位并返回创建的雇员块索引的ObjectId。函数的形式如下(你可以改变参数顺序)
public ObjectId CreateEmployee(string name, string division, double salary, Point3d pos)
2) 移除上面函数中的CommandMethod属性”CREATE”,这样它就不再是用来创建雇员的命令。
3) 修改函数的代码,这样就可以正确地设置块索引的名字、职位、部门和薪水和它的扩展字典。
· 替换
BlockReference br = new BlockReference(new Point3d(10, 10, 0), CreateEmployeeDefinition());
为
BlockReference br = new BlockReference(pos, CreateEmployeeDefinition());
· 替换
xRec.Data = new ResultBuffer(
new TypedValue((int)DxfCode.Text, "Earnest Shackleton"),
new TypedValue((int)DxfCode.Real, 72000),
new TypedValue((int)DxfCode.Text, "Sales"));
为
xRec.Data = new ResultBuffer(
new TypedValue((int)DxfCode.Text, name),
new TypedValue((int)DxfCode.Real, salary),
new TypedValue((int)DxfCode.Text, division));
4) 因为我们把雇员的名字从MText替换成块的属性定义,因此我们要创建一个相应的属性索引来显示雇员的名字。属性索引将使用属性定义的属性。
· 替换:
btr.AppendEntity(br); //加入索引到模型空间
trans.AddNewlyCreatedDBObject(br, true); //让事务处理知道
为
AttributeReference attRef = new AttributeReference();
//遍历雇员块来查找属性定义
BlockTableRecord empBtr = (BlockTableRecord)trans.GetObject(bt["EmployeeBlock"], OpenMode.ForRead);
foreach (ObjectId id in empBtr)
{
Entity ent = (Entity)trans.GetObject(id, OpenMode.ForRead, false);
//打开当前的对象!
if (ent is AttributeDefinition)
{
//设置属性为属性索引中的属性定义
AttributeDefinition attDef = ((AttributeDefinition)(ent));
attRef.SetPropertiesFrom(attDef);
attRef.Position = new Point3d(attDef.Position.X + br.Position.X, attDef.Position.Y + br.Position.Y, attDef.Position.Z + br.Position.Z);
attRef.Height = attDef.Height;
attRef.Rotation = attDef.Rotation;
attRef.Tag = attDef.Tag;
attRef.TextString = name;
}
}
//把索引加入模型空间
btr.AppendEntity(br);
//把属性索引加入到块索引
br.AttributeCollection.AppendAttribute(attRef);
//让事务处理知道
trans.AddNewlyCreatedDBObject(attRef, true);
trans.AddNewlyCreatedDBObject(br, true);
研究一下上面的代码,看看是怎样把属性定义中除显示用的文本字符串外的属性复制到属性索引的。属性被加入到块索引的属性集合中。这就是你怎样来为每一个实例自定义雇员名字。
5)不要忘记返回雇员块索引的ObjectId,但要在提交事务处理之后才能返回:
trans.Commit();
return br.ObjectId;
6) 测试CreateEmployee。
加入一个Test命令来测试CreateEmployee:
[CommandMethod("Test")]
public void Test()
{
CreateEmployee("Earnest Shackleton", "Sales", 10000, new Point3d(10, 10, 0));
}
修改CreateDivision()以重用:
让我们来修改CreateDivision ()函数,以让它可以接收部门名字、经理名字并返回创建的部门经理扩展记录的ObjectId。如果部门经理已经存在,则不改变经理的名字。
1) 如果你先前在CreateEmployeeDefinition()中调用了CreateDivision(),请把它注释掉,因为我们在这里不需要创建一个部门
2) 改变CreateDivision()的形式让它接收部门和经理的名字并返回一个ObjectId。
public ObjectId CreateDivision(string division, string manager)
3) 修改上面函数的代码创建部门的名字和经理:
· 替换:
divDict = (DBDictionary)trans.GetObject(acmeDict.GetAt("Sales"), OpenMode.ForWrite);
为:
divDict = (DBDictionary)trans.GetObject(acmeDict.GetAt(division), OpenMode.ForWrite);
· 替换:
acmeDict.SetAt("Sales", divDict);
为:
acmeDict.SetAt(division, divDict);
· 替换:
mgrXRec.Data = new ResultBuffer(new TypedValue((int)DxfCode.Text, "Randolph P. Brokwell"));
为:
mgrXRec.Data = new ResultBuffer(new TypedValue((int)DxfCode.Text, manager));
不要忘了返回部门经理这个扩展记录的ObjectId,但要在提交事务处理后才返回。
trans.Commit();
//返回部门经理这个扩展记录的ObjectId
return mgrXRec.ObjectId;
现在把在中CreateEmployeeDefinition调用的CreateDivision函数给注释掉。
4) 现在通过使用TEST命令来测试调用CreateDivision函数。使用ArxDbg工具来检查条目是否已被加入到“ACME_DIVISION”下的命名对象字典。
CreateDivision("Sales", "Randolph P. Brokwell")
使用CREATE命令来创建雇员:
我们将加入一个名为CREATE的新命令,此命令用来提示输入雇员的详细资料来创建雇员块索引。让我们来看一下这个命令是怎样使用的。
1) 让我们加入一个名为CREATE的新命令,并声明几个常用的变量和一个try-finally块。
[CommandMethod("CREATE")]
public void CreateEmployee()
{
Database db = HostApplicationServices.WorkingDatabase;
Editor ed = Application.DocumentManager.MdiActiveDocument.Editor;
Transaction trans = db.TransactionManager.StartTransaction();
try
{
trans.Commit();
}
finally
{
trans.Dispose();
}
}
2) 让我们来为雇员定义可以用作为提示缺省值的常数。注意,布尔值gotPosition是用来判断用户是否已输入职位。
. 雇员名 - 类型 :String -缺省值 “Earnest Shackleton”
. 雇员所在部门名 - 类型:String -缺省值“Sales”
. 薪水 -类型:Double (non-negative and not zero) -缺省值10000
. 职位 -类型:Point3d -缺省值(0,0,0)
把这些常数加入到try语句后面:
string empName = "Earnest Shackleton";
string divName = "Sales";
double salary = new double();
salary = 10000;
Point3d position = new Point3d(0, 0, 0);
bool gotPosition = new bool();
//布尔值用来判断用户是否已输入职位
gotPosition = false;
3) 现在让我们提示用户输入值。我们先使用PromptXXXOptions类来初始化要显示的提示字符串。
//提示输入每个雇员的详细资料
PromptStringOptions prName = new PromptStringOptions("Enter Employee Name <" + empName + ">");
PromptStringOptions prDiv = new PromptStringOptions("Enter Employee Division <" + divName + ">");
PromptDoubleOptions prSal = new PromptDoubleOptions("Enter Employee Salary <" + salary + ">");
PromptPointOptions prPos = new PromptPointOptions("Enter Employee Position or");
注意,提示字符串用尖括号来显示变量的值。这是AutoCAD用来提示用户这个值为缺省值。
4) 当提示用户输入职位时,我们也提供了一个关键字列表选项,如名字、部门和薪水。如果用户想要在选择一个点的时候改变为其它值,他可以选择那个关键字。
一个命令提示的例子如下:
Command: CREATE
Enter Employee Position or [Name/Division/Salary]:
要创建一个雇员,用户会选择一个点而其它的值被设置为缺省值。如果用户要改变其它的值,如名字,他可以输入”N”或全名”Name”,然后输入名字:
Command: CREATE
Enter Employee Position or [Name/Division/Salary]:N
Enter Employee Name <Earnest Shackleton>:
如果用户想要再次选择缺省的名字,他可以按回车键。
让我们创建用于职位提示的关键字列表:
//加入用于职位提示的关键字
prPos.Keywords.Add("Name");
prPos.Keywords.Add("Division");
prPos.Keywords.Add("Salary");
//设置提示的限制条件
prPos.AllowNone = false; //不允许没有值
5) 现在让我们声明PromptXXXResult变量来获取提示的结果:
//prompt results
PromptResult prNameRes;
PromptResult prDivRes;
PromptDoubleResult prSalRes;
PromptPointResult prPosRes;
6) 直到用户成功输入一个点后,循环才结束。如果输入错误的话,我们会提示用户并退出函数:
判断用户是否输入了关键字,我们通过检查promptresult的状态来进行:
//循环用来获取雇员的详细资料。当职位被输入后,循环终止。
while (!gotPosition)
{
//提示输入职位
prPosRes = ed.GetPoint(prPos);
//取得一个点
if (prPosRes.Status == PromptStatus.OK)
{
gotPosition = true;
position = prPosRes.Value;
}
else if (prPosRes.Status == PromptStatus.Keyword) //获取一个关键字
{
//输入了Name关键字
if (prPosRes.StringResult == "Name")
{
//获取雇员名字
prName.AllowSpaces = true;
prNameRes = ed.GetString(prName);
if (prNameRes.Status != PromptStatus.OK)
{
return;
}
//如果获取雇员名字成功
if (prNameRes.StringResult != "")
{
empName = prNameRes.StringResult;
}
}
}
else
{
// 获取职位时发生错误
ed.WriteMessage("***Error in getting a point, exiting!!***" + "/r/n");
return;
} // 如果获取一个点
}
7) 上面的代码只提示输入名字,请加入提示输入薪水和部门的代码。
8) 完成提示输入后,我们将使用获得的值来创建雇员。
//创建雇员
CreateEmployee(empName, divName, salary, position);
9) 现在来检查部门经理是否已存在。我们通过检查NOD中部门的扩展记录中的经理名字来进行。如果检查到的是一个空字符串,那么我们会提示用户输入经理的名字。注意,通过修改CreateDivision()函数,获取经理的名字变得简单了。
string manager = "";
//创建部门
//给经理传入一个空字符串来检查它是否已存在
Xrecord depMgrXRec;
ObjectId xRecId;
xRecId = CreateDivision(divName, manager);
//打开部门经理扩展记录
depMgrXRec = (Xrecord)trans.GetObject(xRecId, OpenMode.ForRead);
TypedValue[] typedVal = depMgrXRec.Data.AsArray();
foreach (TypedValue val in typedVal)
{
string str;
str = (string)val.Value;
if (str == "")
{
//经理没有被设置,现在设置它
// 先提示输入经理的名字
ed.WriteMessage("/r/n");
PromptStringOptions prManagerName = new PromptStringOptions("No manager set for the division! Enter Manager Name");
prManagerName.AllowSpaces = true;
PromptResult prManagerNameRes = ed.GetString(prManagerName);
if (prManagerNameRes.Status != PromptStatus.OK)
{
return;
}
//设置经理的名字
depMgrXRec.Data = new ResultBuffer(new TypedValue((int)DxfCode.Text, prManagerNameRes.StringResult));
}
}
10) 测试CREATE命令
选择集:
现在让我们来创建一个命令,当用户在图形中选择一个雇员对象时,它会显示雇员的详细资料。
我们会使用上一章中创建的ListEmployee()函数在命令行中输出雇员的详细资料。
下面是你必须遵循的步骤:
1. 调用“LISTEMPLOYEES”命令
2. 调用Editor的GetSelection()函数来选择实体
PromptSelectionResult res = ed.GetSelection(Opts, filter);
3. 上面的filter用来过滤选择集中的块索引。你可以创建如下的过滤列表:
TypedValue[] filList = new TypedValue[1];
filList[0] = new TypedValue((int)DxfCode.Start, "INSERT");
SelectionFilter filter = new SelectionFilter(filList);
4. 从选择集中获取ObjectId数组:
//如果选择失败则什么也不做
if (res.Status != PromptStatus.OK)
return;
Autodesk.AutoCAD.EditorInput.SelectionSet SS = res.Value;
ObjectId[] idArray;
idArray = SS.GetObjectIds();
5. 最后,把选择集中的每个ObjectId输入到ListEmployee()函数来获取一个雇员详细资料的字符串数组。把雇员的详细资料输出到命令行。例如:
//获取saEmployeeList 数组中的所有雇员
foreach (ObjectId employeeId in idArray)
{
ListEmployee(employeeId, ref saEmployeeList);
//把雇员的详细资料输出到命令行
foreach (string employeeDetail in saEmployeeList)
{
ed.WriteMessage(employeeDetail);
}
ed.WriteMessage("----------------------" + "/r/n");
}
Autodesk官方最新的.NET教程(六)(C#版)
第6章 更多的用户界面:添加自定义数据
在本章中,我们将介绍.NET API的用户界面部分能做些什么。我们首先将介绍一个自定义上下文菜单(快捷菜单)。接下来我们将实现一个无模式可停靠的面板(一个真正的AutoCAD增强辅助窗口)来支持拖放操作。接着我们将介绍通过模式窗体选取实体。最后,我们将介绍使用AutoCAD的选项对话框来设置雇员的缺省值。
本章还会介绍和上面内容有关的API。
第一部分 自定义上下文菜单
到目前为止,我们所写的代码只与CommandMethod属性定义的命令行进行相互操作。一个AutoCAD .NET程序能通过一个特殊的类来实现装载时的初始化工作。这个类只要实现IExtensionApplication .NET接口并暴露一个组件级别的属性(此属性把类定义为ExtensionApplication),就可以响应一次性的装载和卸载事件。例子:
[assembly: ExtensionApplication(typeof(Lab6_CS.AsdkClass1))]
public classAsdkClass1 : IExtensionApplication
{
1) 现在修改AsdkClass1类来实现上面的接口。要实现这个接口,你必须实现Initialize() 和Terminate()函数。因为我们要实现的是一个接口,而接口中的函数总是定义为纯虚拟的。
public voidInitialize()
{
AddContextMenu();
EmployeeOptions.AddTabDialog();
}
public voidTerminate()
{
}
为了加入自定义上下文菜单,我们必须定义一个‘ContextMenuExtension’类的成员。这个类位于Autodesk.AutoCAD.Windows命名空间中。
要使用ContextMenuExtension,我们必须使用new关键字来进行初始化,给必要的属性赋值,并调用Application.AddDefaultContextMenuExtension()。上下文菜单的工作方式是:对于每个菜单条目,我们定义一个成员函数来处理菜单单击事件。我们可能通过.NET的代理来实现。我们使用C#关键字+=和-=确定让哪个函数来处理事件。请尽快熟悉这种设计模式,因为在C#中会使用很多。
2) 添加一个‘ContextMenuExtension’成员变量和下面两个用来添加和移除自定义菜单的函数。请好好研究一下代码来看看究竟发生了什么。
void AddContextMenu()
{
try
{
m_ContextMenu= new ContextMenuExtension();
m_ContextMenu.Title= "Acme Employee Menu";
Autodesk.AutoCAD.Windows.MenuItemmi;
mi= newAutodesk.AutoCAD.Windows.MenuItem("Create Employee");
mi.Click+= newEventHandler(CallbackOnClick);
m_ContextMenu.MenuItems.Add(mi);
Autodesk.AutoCAD.ApplicationServices.Application.AddDefaultContextMenuExtension(m_ContextMenu);
}
catch
{
}
}
void RemoveContextMenu()
{
try
{
if( m_ContextMenu != null)
{
Autodesk.AutoCAD.ApplicationServices.Application.RemoveDefaultContextMenuExtension(m_ContextMenu);
m_ContextMenu= null;
}
}
catch
{
}
}
注意我们在代码中使用了‘CallbackOnClick’函数。我们希望这个函数(我们现在还没有定义)响应菜单项选择事件。在我们的例子中,我们想要做的是调用我们的成员函数‘Create()’。请加入下面的代码。
void CallbackOnClick(objectSender, EventArgs e)
{
Create();
}
现在,从Initialize()中调用AddContextMenu()函数,同样地,请在Terminate()中调用RemoveContextMenu()。
请运行代码。使用NETLOAD来装载编译好的组件,然后在AutoCAD的空白处右击……你应该可以看到’Acme‘快捷菜单了。如果失败了,明明你做的都是正确的……为什么呢?
通常,AutoCAD的数据是存储在文档中的,而访问实体的命令有权修改文档。当我们运行代码来响应上下文菜单单击事件,我们是从命令结构的外部来访问文档。当我们调用的代码尝试通过添加一个雇员来修改文档时,我们就碰到了错误。正确的做法是必须锁住文档,这可以通过使用Document.LockDocument()命令来实现。
3) 修改CallbackOnClick来锁住文档:
void CallbackOnClick(objectSender, EventArgs e)
{
DocumentLockdocLock =Autodesk.AutoCAD.ApplicationServices.Application.DocumentManager.MdiActiveDocument.LockDocument();
Create();
docLock.Dispose();
}
注意,我们保留了一个‘DocumentLock’对象的拷贝。要把文档解锁,我们只要销毁这个DocumentLock对象。
再次运行代码。现在应该可以看到快捷菜单了。
第2部分 无模式对话框、可进行拖放的可停靠面板
为了使我们的用户界面和AutoCAD实现无缝链接,我们要尽可能在所有的地方使用同样的用户界面结构。这会使应用程序看起来与AutoCAD结合的很好,并有效地减少代码的重复。一个很好的例子是AutoCAD中的可停靠面板。
使用.NET API,我们可以创建一个简单的窗体并把它放到面板中。我们可以实例化一个自定义的‘PaletteSet’对象来包含窗体,并可以把这个PaletteSet定义成我们喜欢的样式。
4) 在解决方案浏览器中通过右击工程来添加一个用户控件。给它命名为ModelessForm。使用控件工具箱,加入如下所示的编辑框和标签控件。
使用属性窗口设置三个编辑框的属性。设置如下:
<首先是最上面的编辑框>
(Name) = tb_Name
Text = <请输入一个名字>
<第二个编辑框>
(Name) = tb_Division
Text = Sales
<第三个编辑框>
(Name) = tb_Salary
Text = <请输入薪水>
要使用.NET API实例化一个面板对象,你必须要实例化用户控件对象(无模式窗体)和‘PaletteSet’对象。调用PaletteSet的成员函数Add来传递用户控件对象。
5) 接下来,我们要加入一个命令来创建面板。在类中加入一个名为CreatePalette的函数和CommandMethod属性来定义名为“PALETTE”的命令。
请看一下下面的代码块。这是实例化面板的代码。
ps = new Autodesk.AutoCAD.Windows.PaletteSet("TestPalette Set");
ps.MinimumSize = newSystem.Drawing.Size(300, 300);
System.Windows.Forms.UserControl myCtrl = new ModelessForm();
ps.Add("test", myCtrl);
ps.Visible = true;
6) 把上面的代码加入到CreatePalette()函数。‘ps’需要在函数的外部声明:
private Autodesk.AutoCAD.Windows.PaletteSet ps;
在函数的实例化面板代码之前加入检查ps是否为null的代码。
编译并运行工程。在AutoCAD中装载组件,运行‘PALETTE’命令来检查面板是否被装载。
使用PaletteSet.Style来看看PaletteSetStyles对象。例如:
ps.Style = PaletteSetStyles.ShowTabForSingle;
我们也可以试试诸如透明性的属性,例如:
ps.Opacity =90;
注意:要使用PaletteSet 和PaletteSetStyles对象,你必须加入两个命名空间Autodesk.AutoCAD.Windows和Autodesk.AutoCAD.Windows.Palette
在我们继续之前,让我们执行一个快速的维护更新:请在AsdkClass1类中加入下列成员:
public static string sDivisionDefault = "Sales";
public static string sDivisionManager = "Fiona Q.Farnsby";
这些值将被用作为部门和部门经理的缺省值。由于它们被声明为’static’,它们在每个程序中只实例化一次,并在组件装载的时候实例化。
第2a部分 在无模式窗体中加入拖放支持
在这部分,我们将加入允许我们使用面板窗体中编辑框的值来创建一个雇员。当用户从面板中拖动到AutoCAD中,将会提示输入职位,一个新的雇员实体将使用这些值来进行创建。
7) 为了支持拖放,我们首先需要一个对象来进行拖动。在编辑框的下面,另外加入一个名为Label4的标签控件,设置标签的文本为一些提示性的东西(‘Drag to Create Employee’)。通过这个标签,我们可以在AutoCAD中处理拖放。
要捕捉到什么时候拖动事件发生,我们必须要知道什么时候鼠标开始操作。
首先,我们要在类的构造函数中注册事件,代码如下:
Label4.MouseMove+= newSystem.Windows.Forms.MouseEventHandler(Label4_MouseMove);
8) 在ModelessForm类中加入下面的函数声明:
private voidLabel4_MouseMove( object sender,System.Windows.Forms.MouseEventArgs e)
{
if (System.Windows.Forms.Control.MouseButtons ==System.Windows.Forms.MouseButtons.Left)
{
// start dragDrop operation, MyDropTarget will be calledwhen the cursor enters the AutoCAD view area.
Autodesk.AutoCAD.ApplicationServices.Application.DoDragDrop(this, this,System.Windows.Forms.DragDropEffects.All, newMyDropTarget());
}
}
通常事件处理器有2个输入参数,一个object类的sender和与事件有关的参数。对于MouseMove,我们也要做同样的事情。
运行这个工程,检查一下当鼠标经过文本的时候,函数是否被调用的。
我们还可以进一步知道是不是按了鼠标左键:
if (System.Windows.Forms.Control.MouseButtons ==System.Windows.Forms.MouseButtons.Left)
{
}
我们需要一个方法来检测什么时候对象被拖入到AutoCAD。我们可以使用.NET的基类DropTarget来实现。要使用它,你只要创建从这个基类派生的类并实现你想要的函数。在我们这个例子中,我们需要的是OnDrop()。
9) 在工程中加入一个从Autodesk.AutoCAD.Windows.DropTarget派生的类‘MyDropTarget’。如果你把这个类加入到ModelessForm.cs文件中,请把这个类加入到ModelessForm类之后。
override public void OnDrop(System.Windows.Forms.DragEventArgs e)
{
}
在这个函数中,我们最后会调用AsdkClass1的成员CreateDivision() 和CreateEmployee,传入ModelessForm类中的编辑框的值。要实现这个功能,我们需要一个方法来连接ModelessForm实例。最佳的方法是通过DragEventArgs。但首先我们要把鼠标事件连接到MyDropTarget类。
10) 加入下面的代码到鼠标左键(MouseButtons.Left)处理函数中:
Autodesk.AutoCAD.ApplicationServices.Application.DoDragDrop(this, this,System.Windows.Forms.DragDropEffects.All, newMyDropTarget());
注意我们传入’this’两次。第一次是用于Control参数,第二次是用于传入用户自定义数据。因为我们传入的是ModelessForm 类的实例,所以我们可以在放下的时候使用它来获取编辑框的值。
11) 回到OnDrop处理函数,让我们使用参数来调用创建雇员的函数。首先,添加职位提示的代码。在AsdkClass1.Create()中已经有相关的代码了,位于‘Get EmployeesCoordinates…’.注释下面。添加此代码来提示输入职位。
12) 接下来,获取传入到DragEventArgs 参数的ModelessForm对象:
ModelessFormctrl = (ModelessForm)e.Data.GetData(typeof(ModelessForm));
请注意一下怎样通过typeof关键字把参数强制转化为ModelessForm的实例。
13) 使用上面的实例来调用AsdkClass1成员:
AsdkClass1.CreateDivision(ctrl.tb_Division.Text,AsdkClass1.sDivisionManager);
AsdkClass1.CreateEmployee(ctrl.tb_Name.Text,ctrl.tb_Division.Text, Convert.ToDouble(ctrl.tb_Salary.Text), prPosRes.Value);
注意:AsdkClass1的方法要不通过AsdkClass1的实例来调用,那么方法必须被声明为’ public static’。因为public static 方法只能调用其它的public static 方法,你需要修改几个AsdkClass1类中的方法为’ public static’。请你进行相关的修改(应该至少有4项要修改)。
14) 最后,因为我们处理的事件位于AutoCAD命令之外,我们必须再次在会修改数据库的代码处锁住文档。请加入锁住文档的代码,加入的方法与前面的上下文菜单是一样的。
编译、装载并运行组件,使用PALETTE命令。你应该可以使用拖放操作来创建一个雇员了。
第三部分 从有模式窗体中选择实体
本章的以下部分将演示获取一个用户在屏幕上选择的雇员实例的详细信息,并把信息显示在一个有模式窗体的编辑框中。这部分的重点是创建一个有模式窗体,并在执行选择操作而窗体要失去焦点时隐藏它。为了获取雇员的详细信息,我们将使用第4章结束时给出的ListEmployee帮助函数。
首先,我们需要创建一个窗体类。这个类是一个真实的窗体而不是我们在ModelessForm中创建的用户控件。
15) 在工程中创建一个Windows窗体类。调用‘ModalForm’类。在窗体中加入以下所示的三个编辑框控件和标签控件以及两个按钮。
使用属性窗口来设置三个编辑框的属性。设置如下:
<首先是最上面的编辑框>
(Name) = tb_Name
Text = <空白>
<第二个编辑框>
(Name) = tb_Division
Text = <空白>
<第三个编辑框>
(Name) = tb_Salary
Text = <空白>
<上部的按钮>
(Name) = SelectEmployeeButton
Text= Select Employee
<下部的按钮>
(Name) = Close
Text= Close
接下来创建按钮的事件处理函数。‘Close’按钮可以只简单地调用:
this.Close();
要显示对话框,让我们在类中创建一个把窗体实例化为有模式对话框的命令函数。下面的实现的代码:
[CommandMethod("MODALFORM")]
public voidShowModalForm()
{
ModalFormmodalForm = new ModalForm();
Autodesk.AutoCAD.ApplicationServices.Application.ShowModalDialog(modalForm);
}
编译、装载并在AutoCAD中运行MODALFORM命令来看看对话框是否可以工作。试试在对话框的右下角调整对话框的大小,然后关闭它。注意,重新使用MODALFORM命令时,对话框会出现在你上次离开的地方!这是ShowModalDialog方法的一个特征。大小和位置值被AutoCAD保存了。
‘Select Employee’按钮首先将执行一个简单的实体选择。这我们可以通过使用Editor.GetEntity()方法来实现,选择单一的实体比使用选择集来得方便的多。下面是怎样使用这个方法的代码:
PromptEntityOptionsprEnt = new PromptEntityOptions("Select anEmployee");
PromptEntityResultprEntRes = ed.GetEntity(prEnt);
16) 把上面的代码加入到SelectEmployeeButton_Click处理函数中,还要加入必需的数据库、命令行、事务处理设置变量和一个try catch块。不要忘了在finally块中销毁它们。
使用PromptStatus.OK来测试GetEntity的返回值,如果返回不等于,就调用this.Show并退出处理函数。
一旦我们获得的返回值是OK,那么我们就可以使用PromptEntityResult.ObjectId()方法来获取所选实体的object Id。这个id可以和一个固定的字符串数组被传入到AsdkClass1.ListEmployee函数中来获取雇员的详细信息。可以通过以下的代码说明:
ArrayListsaEmployeeList = new ArrayList();
AsdkClass1.ListEmployee(prEntRes.ObjectId,saEmployeeList);
if (saEmployeeList.Count == 4)
{
tb_Name.Text= saEmployeeList[0].ToString();
tb_Salary.Text= saEmployeeList[1].ToString();
tb_Division.Text= saEmployeeList[2].ToString();
}
17) 加入上面的代码,它会在窗体的编辑框中显示雇员的详细信息。
在开始测试代码之前,我们还要记住的是代码是在有模式对话框中运行的,也就意味着当对话框可见的时候用户与AutoCAD的互操作是被禁止的。在用户能够进行选择雇员对象之前,我们必须隐藏窗体。当选择结束后,我们可以再次站窗体显示(例如,可以在finally块的函数中)
18) 在选择之前加入隐藏窗体的代码(例如在try块之前) ‘this.Hide’ 和选择结束后显示窗体的代码(例如,可以在finally块中)‘this.Show’。
编译、装载并在AutoCAD中运行MODALFORM命令来看看对话框是否工作。试试选择一个实体并填充窗体中编辑框的值。
第四部分 在AutoCAD选项对话框中加入页面
本章的最后部分将向你介绍如何定义一个用户控件,这个控件可以被作为一个页面显示在AutoCAD的选项对话框中。我们可以使用这个页面来设置程序运行期间的缺省值。在Employee例子中,我们只是在AsdkClass1类中简单地设置了sDivisionDefault 和sDivisionManager字符串。
19) 在工程中加入另外一个名为‘EmployeeOptions’的用户控件。在控件中加入两个编辑框和标签控件,如下图所示:
使用属性窗口来设置编辑框的属性,设置如下:
<上面的编辑框>
(Name) = tb_EmployeeDivision
Text = <空白>
<下面的编辑框>
(Name) = tb_DivisionManager
Text = <空白>
使用.NET API来显示自定义多页对话框,需要两个步骤。首先,通过传入要调用的成员函数的地址,来知道什么时候选项对话框出现。其次是实现回调函数。传入到回调函数中的第二个参数是一个‘TabbedDialogEventArgs’对象,我们必须使用它来调用‘AddTab’函数。AddTab使用一个标题字符串和一个‘TabbedDialogExtension’对象的实例,此实例封装了我们的窗体(其实是用户控件)。在TabbedDialogExtension的构造函数中,我们输入窗体的实例和回调函数(OnOK, OnCancel 或OnHelp)的地址。
20) 在EmployeeOptions类中,加入一个名为AddTabDialog的public static函数,它会添加一个可供系统调用的事件处理:
public static void AddTabDialog()
{
Autodesk.AutoCAD.ApplicationServices.Application.DisplayingOptionDialog+= new TabbedDialogEventHandler(TabHandler);
}
在AsdkClass1的Initialize函数中加入调用此函数的代码。因为这个函数是在程序启动的时候调用的(因为类已经实现了IExtensionApplication接口),所以多页对话框就被自动的加载。
20a) 实现一个相同的函数来移除事件处理,使用C#的-=关键字。
在这里,你可以看到我们为AutoCAD中的Application 对象的DisplayingOptionDialog事件加入了一个处理函数,此函数会调用‘TabHandler’函数。所以接下来我们要实现这个函数。
21) 加入下面的代码来实现处理函数:
private static void TabHandler(objectsender, Autodesk.AutoCAD.ApplicationServices.TabbedDialogEventArgs e)
{
EmployeeOptionsEmployeeOptionsPage = new EmployeeOptions();
e.AddTab("AcmeEmployee Options",
new TabbedDialogExtension(
EmployeeOptionsPage,
new TabbedDialogAction(EmployeeOptionsPage.OnOk)));
}
我们首先实例化了一个EmployeeOptions对象。然后调用e.AddTab(),在这个函数中传入了一个TabbedDialogExtension的实例。TabbedDialogExtension的构造函数使用了EmployeeOptionsPage实例和一个TabbedDialogAction对象。TabbedDialogAction对象的参数可以是Ok, Cancel 或Help回调函数。在这个函数中,我们使用的是OK。
22) 现在剩下的就是确定回调函数的内容,也就是ONOK的内容。前面已经说过了,我们只要设置AsdkClass1的static成员,也就是设置tb_DivisionManager 和tb_EmployeeDivision编辑框中的值。下面是代码:
public void OnOk()
{
AsdkClass1.sDivisionDefault= tb_EmployeeDivision.Text;
AsdkClass1.sDivisionManager= tb_DivisionManager.Text;
}
编译、装载并选择AutoCAD的选项菜单项来看一下我们的自定义对话框。试试设置对话框中的值并实例化一个雇员。你可以使用PRINTOUTEMPLOYEE命令来查看详细信息。
附加的问题:怎样让对话框的编辑框能自动显示为AsdkClass1中的Manager和Division字符串的内容?
Autodesk官方最新的.NET教程(七)(C#版)
第7章 事件
本章将讨论AutoCAD中的事件。我们将介绍事件处理函数的使用,特别是监视AutoCAD命令的事件处理函数和监视被AutoCAD命令修改的对象的事件处理函数。在解释怎样在C#中实现AutoCAD的事件处理之前,我们将首先简要地讨论一下.NET中的事件。
第一部分 C#中的事件
事件只是用来通知一个行为已经发生的信息。在ObjectARX中,我们使用反应器(reactor)来处理AutoCAD的事件。而在AutoCAD .NET API中,ObjectARX反应器被换成了事件。
事件处理函数(或者叫回调函数)是用来监视和反馈程序中出现的事件。事件可以以不同的形式出现。
在介绍AutoCAD .NET API中的事件之前,让我们先来简单地了解一下代理。
第1a部分 代理
代理是一个存储方法索引的类(概念与函数指针类似)。代理对方法是类型安全的(与C中的函数指针类似)。代理有特定的形式和返回类型。代理可以封装符合这种特定形式的任何方法。
代理的一个用途就是作为产生事件的类的分发器。事件是.NET环境中第一级别的对象。虽然C#把事件处理的许多细节给隐藏掉了,但事件总是由代理来实现的。事件代理可以多次调用(就是它们可以存储多于1个的事件处理方法的索引)。它们保存了用于事件的一个注册事件处理的列表。一个典型的代理有以下的形式:
public delegate Event (Object sender, EventArgs e)
第一个参数sender表示引发事件的对象。第二个参数e是一个EventArgs参数(或者是一个派生的类),这个对象通常包含用于事件处理函数的数据。
第1b部分 +=和-=语句
要使用事件处理函数,我们必须把它与事件联系起来。这要通过使用+=语句。+=和-=允许你在运行时连接、断开或修改与事件联系的处理函数。
当我们使用+=语句时,我们要确定事件引发者的名字,并要使用new语句来确定事件处理函数,例如:
MyClass1.AnEvent += new HandlerDelegate(EHandler)
前面我们说过要使用-=语句从事件处理函数中断开事件(移除联系)。语法如下所示:
MyClass1.AnEvent -= new HandlerDelegate(EHandler)
第2部分 处理.NET中的AutoCAD事件
在ObjectARX中,我们使用反应器来封装AutoCAD事件。在AutoCAD .NET API中,我们可以使用事件来代替ObjectARX反应器。
通常,处理AutoCAD事件的步骤如下:
1. 创建事件处理函数
当一个事件发生时,事件处理函数(或称为回调函数)被调用。任何我们想要处理的回应AutoCAD事件的动作都在事件处理函数中进行。
例如,假定我们只想通知用户一个AutoCAD对象已被加入。我们可以使用AutoCAD数据库事件”ObjectAppended”来完成。我们可以编写回调函数(事件处理函数)如下:
public void objAppended(object o, ObjectEventArgs e)
{
// 在这里加入处理代码
}
函数中的第一个参数代表AutoCAD数据库。第二个参数代表ObjectEventArgs类,它可能包含对处理函数有用的数据。
2. 把事件处理函数与事件联系起来
为了开始监视动作,我们必须把事件处理函数与事件联系起来。在这里,当一个对象加入到数据库时,ObjectAppended事件将会发生。但是,事件处理函数不会响应这个事件,除非我们把它与这个事件联系起来,例如:
Database db;
db = HostApplicationServices.WorkingDatabase;
db. ObjectAppended += new ObjectEventHandler(objAppended);
3. 断开事件处理函数
要终止监视一个动作,我们必须断开事件处理函数与事件的联系。当对象被加入时,我们想要停止通知用户这个事件,我们要断开事件处理函数与事件ObjectAppended的联系。
db. ObjectAppended -= new ObjectEventHandler(objAppended);
第3部分 使用事件处理函数来控制AutoCAD的行为
本章的目的是解释AutoCAD事件怎样才能被用于控制AutoCAD图形中的行为。现在,让我们使用前一章(第六章)的内容在AutoCAD图形中创建几个EMPLOYEE块索引。我们不想让用户能改变EMPLOYEE块索引的位置,而对于其它的非EMPLOYEE块索引的位置则没有这个限制。我们将混合使用数据库与文档事件来做到这一点。
首先,我们想要监视将要被执行的AutoCAD命令(使用CommandWillStart事件)。特别地,我们要监视MOVE命令。另外,当一个对象要被修改时,我们应该被通知(使用ObjectOpenedForModify事件),这样我们可以确定它是否为一个EMPLOYEE块索引。如果这时就修改对象可能是无效的,因为我们的修改可能会再次触发事件,从而引起不稳定的行为。所以,我们要等待Move命令的执行结束(使用CommandEnded事件),这时就可以安全地修改对象了。当然,任何对块索引的修改将会触发ObjectOpenedForModify事件。我们还需要设置一些全局变量来表明一个MOVE命令在运行和被修改的对象是一个EMPLOYEE块索引。
注意:因为本章需要比较多的代码来获得想要的结果,所以我们不会解释任何与事件处理无关的代码,而只是将它们粘贴到事件处理函数中。这里的重点是成功创建和注册事件处理函数。
第一步:创建新工程
我们以第六章的工程开始。请新加入一个类AsdkClass2。我们还要加入四个全局变量。前两个是Boolean型的:一个用来表示我们监视的命令是否是活动的,另外一个用来表示ObjectOpenedForModify事件处理函数是否该被忽略。
//全局变量
bool bEditCommand;
bool bDoRepositioning;
接下来,我们要声明一个全局变量来表示一个ObjectIdCollection,它用来存储我们所选择的要修改的对象的ObjectID。
ObjectIdCollection changedObjects = new ObjectIdCollection();
最后,我们要声明一个全局变量来表示一个Point3dCollection,它用来包含我们所选对象的位置(三维点)。
Point3dCollection employeePositions = new Point3dCollection();
第2步:创建第一个文档事件处理函数(回调函数)
现在我们要创建一个事件处理函数。当AutoCAD命令开始执行的时候它会通知我们。我们要检查GlobalCommandName的值是否为MOVE。
if ( e.GlobalCommandName == "MOVE" )
{
}
如果MOVE命令开始执行的话,我们要相应地设置Boolean变量bEditCommand的值,这样我们可以知道我们所监视的命令是活动的。同样地,我们应该把另外一个Boolean变量bDoRepositioning设置为false来忽略ObjectOpenedForModify事件处理函数。两个变量设置好以后,在命令活动期间,我们必须要获得所选块索引的信息。
我们还应该把两个集合对象的内容清空。我们只关心当前选择的对象。
第3步: 创建数据库事件处理函数(回调函数)
无论什么时候一个对象被打开并要被修改时,数据库事件处理函数会被调用。当然,如果这时我们监视的命令不是活动的,我们就应该跳过任何被这个回调函数调用的内容。
if ( bEditCommand == false )
{
return;
}
同样地,如果我们监视的命令已经结束,而ObjectOpenedForModify事件被另一个回调函数再次触发的话,而这时有对象被修改时,我们要阻止所有由这个回调函数执行的动作。
if ( bDoRepositioning == true )
{
return;
}
这个回调函数剩余部分的代码用来验证我们是否正在处理EMPLOYEE块索引。如果是的话,我们就获取它的ObjectID和位置(三维点)。下面的代码可以被粘贴到这个事件处理函数函数。
public void objOpenedForMod(object o, ObjectEventArgs e)
{
if ( bEditCommand == false )
{
return;
}
if ( bDoRepositioning == true )
{
return;
}
ObjectId objId;
objId = e.DBObject.ObjectId;
Transaction trans;
Database db;
db = HostApplicationServices.WorkingDatabase;
trans = db.TransactionManager.StartTransaction();
using(Entity ent = (Entity)trans.GetObject(objId, OpenMode.ForRead, false))
{
if ( ent.GetType().FullName.Equals( "Autodesk.AutoCAD.DatabaseServices.BlockReference" ) )
{ //We use .NET//s RTTI to establish type.
BlockReference br = (BlockReference)ent;
//Test whether it is an employee block
//open its extension dictionary
if ( br.ExtensionDictionary.IsValid )
{
using(DBDictionary brExtDict = (DBDictionary)trans.GetObject(br.ExtensionDictionary, OpenMode.ForRead))
{
if ( brExtDict.GetAt("EmployeeData").IsValid )
{
//successfully got "EmployeeData" so br is employee block ref
//Store the objectID and the position
changedObjects.Add(objId);
employeePositions.Add(br.Position);
//Get the attribute references,if any
AttributeCollection atts;
atts = br.AttributeCollection;
if ( atts.Count > 0 )
{
foreach(ObjectId attId in atts )
{
AttributeReference att;
using(att = (AttributeReference)trans.GetObject(attId, OpenMode.ForRead, false))
{
changedObjects.Add(attId);
employeePositions.Add(att.Position);
}
}
}
}
}
}
}
}
trans.Commit();
}
第4步 创建第二个文档事件处理函数(回调函数)
当一个命令结束时,第三个事件处理函数被调用。同样地,我们要检查全局变量来验证这个将要结束的命令是我们监视的命令。如果是我们监视的,那么我们要重置这个变量:
if ( bEditCommand == false )
{
return;
}
bEditCommand = false;
这个回调函数执行的动作将会再次触发ObjectOpenedForModify事件。我们必须确定在这个回调函数中跳过了所有与此事件有关的动作。
//设置标志来跳过OpenedForModify处理函数
bDoRepositioning = true;
这个回调函数的剩余代码用来把EMPLOYEE块索引和它的关联属性引用的当前(修改过的)位置与它们的初始位置作比较。如果位置改变了,我们在这个回调函数中把它们重置这初始的位置。下面的代码可以被粘贴到这个事件处理函数中。
public void cmdEnded(object o , CommandEventArgs e)
{
//Was our monitored command active?
if ( bEditCommand == false )
{
return;
}
bEditCommand = false;
//Set flag to bypass OpenedForModify handler
bDoRepositioning = true;
Database db = HostApplicationServices.WorkingDatabase;
Transaction trans ;
BlockTable bt;
Point3d oldpos;
Point3d newpos;
int i ;
for ( i = 0; i< changedObjects.Count; i++)
{
trans = db.TransactionManager.StartTransaction();
using( bt = (BlockTable)trans.GetObject(db.BlockTableId, OpenMode.ForRead) )
{
using(Entity ent = (Entity)trans.GetObject(changedObjects[i], OpenMode.ForWrite))
{
if ( ent.GetType().FullName.Equals("Autodesk.AutoCAD.DatabaseServices.BlockReference") )
{ //We use .NET//s RTTI to establish type.
BlockReference br = (BlockReference)ent;
newpos = br.Position;
oldpos = employeePositions[i];
//Reset blockref position
if ( !oldpos.Equals(newpos) )
{
using( trans.GetObject(br.ObjectId, OpenMode.ForWrite) )
{
br.Position = oldpos;
}
}
}
else if ( ent.GetType().FullName.Equals("Autodesk.AutoCAD.DatabaseServices.AttributeReference") )
{
AttributeReference att = (AttributeReference)ent;
newpos = att.Position;
oldpos = employeePositions[i];
//Reset attref position
if ( !oldpos.Equals(newpos) )
{
using( trans.GetObject(att.ObjectId, OpenMode.ForWrite))
{
att.Position = oldpos;
}
}
}
}
}
trans.Commit();
}
}
第5步 创建命令来注册/断开事件处理函数
创建一个ADDEVENTS命令,使用+=语句来把上面的3个事件处理函数连接到各自的事件。在这个命令中,我们还应该设置全局Boolean变量:
bEditCommand = false;
bDoRepositioning = false;
创建另外一个命令REMOVEEVENTS,使用-=语句把事件处理函数与事件断开。
第6步: 测试工程
要测试这个工程,请使用CREATE命令创建一个或多个EMPLOYEE块索引。如果你要作比较的话,你也可以插入一些非EMPLOYEE的块索引。
在命令行中键入ADDEVENTS命令来执行它。
在命令行中输入MOVE命令,然后选择你想要的块索引。注意,当MOVE命令结束时,EMPLOYEE块索引(包括属性)还留在原处。
执行REMOVEEVENTS命令,然后在试一下MOVE命令。注意,EMPLOYEE块索引现在可以被移动了。
附加的问题:添加一个附加的回调函数,当用户改变EMPLOYEE块索引的”Name”属性时,这个回调函数被触发。