本文将介绍一些窗体切换时的小技巧,希望能对大家有所帮助。
在设计Mobile客户端应用程序时,首先就应该根据应用程序的需求和业务流程制定好应用程序中应该出现哪些窗体,并指定窗体之间的导航关系。
这一点和设计WebForm应用程序非常类似。因为一个WebPage往往独占整个浏览器窗口,而Mobile客户端程序是一个窗体独占设备整个屏幕。
这一步非常重要,如果没有设计好窗体和窗体间的导航关系,会直接给客户一种感觉,让他们觉得我们的应用程序无法满足业务需求。
下面的例子是笔者在为某城市城管大队设计城市监管系统时制作的客户端,图1显示了这个程序的窗体间导航关系。
该系统的客户端要部署在每位城管队员的Smartphone设备上,需要实现的功能包括城管队员的登录、上报案件、核查案件和核实案件,并在必要的时候显示来自总部的消息。
根据这样的需求,可以形成几个主要的窗体,同时在进行进一步的设计时,还会形成更多的窗体,用于摆放必要但并不非常重要的控件。此时,还必须回过头,对窗体间的导航进行进一步设计。比如这里的几个细节窗体,都是后来加入的。
因此,窗体间导航关系的设计也应该成为整个迭代过程中的一个步骤,与其他步骤反复进行,最终形成完整的设计。
所谓的窗体转换,其实包括两种形态。一是显示一个新的窗体,让它覆盖原有窗体;二是关闭一个现有窗体,让其后台窗体暴露出来,成为前台窗体。
通常在窗体导航图上只显示第一种形态,例如图1中,从“案件核实细节”到“录音窗体”,表示的情景是当城管队员需要对案情进行录音时,可以启动一个新的“录音窗体”。而当录音完毕时,当然是关闭“录音窗体”,这时“案件核实细节”会自动浮现出来。这就属于第二种形态的窗体切换了,因此图上并没有显示从“录音窗体”到“案件核实细节”的箭头。
显示一个新的窗体通常有两种方式——调用Show方法或ShowDialog方法。他们的区别主要在于,对Show方法的调用会立即返回,新窗体显示的同时Show方法调用语句后面的代码会得到执行;而对ShowDialog方法的调用会被阻塞,直到新窗体关闭,ShowDialog方法调用语句后面的代码才能得到执行。
这时就涉及到一个抉择问题,究竟使用哪种方法来显示一个新的窗体。
通常建议使用ShowDialog方法。因为窗体间的切换还带来一个新的问题,就是如何在窗体间传递数据。比如图1中的“案件核实细节”实际上是为“案件核实窗体”提供附加的、更丰富的数据,这就需要在关闭“案件核实细节”时将搜集到的信息传递给“案件核实窗体”。
使用对话框无疑是最合适的。我们可以在新窗体中提供一些属性,这样当通过设置DialogResult属性关闭对话框时,可以很方便地取出其中的数据。
例如,有两个窗体Form1和Form2,存在从Form1到Form2的导航。我们可以这样实现Form2:
// using语句(略)
class Form2 : Form
{
private TextBox m_txtName;
public Form2() {...} // 构造器(略)
public string UserName
{
get { return m_txtName.Text; }
set { m_txtName.Text = value; }
}
}
然后可以在Form1种这样去使用它:
// using语句(略)
class Form1 : Form
{
...
private void btnOK_Click(object sender, EventArgs e)
{
Form2 f = new Form2();
f.UserName = m_userName; // 还可以在窗体显示前为其属性赋初值
f.ShowDialog(); // 显示新窗体,代码到这里暂停,直到新窗体关闭
m_userName = f.UserName; // 取出新窗体中的数据
f.Dispose();
}
}
略作观察就会发现,Form1中的这段代码和使用通用对话框一样方便。
正是出于这种原因,在窗体间进行导航时更推荐使用ShowDialog方法。前面所述的“城市监管系统”中所有的窗体都是通过这种方式进行导航的。
窗体的回退属于第二种窗体切换形态。如果使用Show方法显示新窗体,我们需要在新窗体中调用this.Close();来关闭;如果使用ShowDialog方法,则需要在新窗体中设置自己的DialogResult属性:
this.DialogResult = DialogResult.OK;
一旦该属性被设置,新窗体立即关闭,原窗体中对ShowDialog方法的调用返回,并将这里设置的属性值作为返回值传递给原窗体,同时继续执行后面的代码。因此在前面Form2的代码中,我们还可以从对f.ShowDialog方法的调用中取得返回值,并根据返回值结果决定后面进行哪些操作:
Form2 f = new Form2();
f.UserName = m_userName;
DialogResult r = f.ShowDialog();
if(r == DialogResult.OK)
m_userName = f.UserName;
f.Dispose();
另外需要注意的就是最后一行对f.Dispose方法的调用,这是必须的,因为移动设备资源非常有限,必须及时进行释放。
当应用程序启动了多个窗体以后,如果打开“开始”→“设置”→“内存”→“运行的程序”,我们会同时看到我们所启动的所有程序(确切地说是所有窗体)。这样,对应着我们的一个应用程序,则出现了多个窗体。
这对我们调试应用程序是非常有必要的,因为我们可以清晰地知道目前有多少个窗体被覆盖在顶层窗体之下,并针对窗体导航图检查是否符合设计;同时,如果同一时刻出现的窗体数量过多,我们还需要对窗体导航图进行优化,争取做到同时显示的窗体尽量少。
然而,如果程序发布时依然保留这种特征,就会带来很多问题。比如用户可能会通过“运行的程序”界面来手动地把某个窗体激活,这样就干扰了程序中的窗体导航关系。这是我们需要运用一些技巧,在这个界面中隐藏我们的窗体,只暴露一个顶层窗体。
幸运的是,Mobile系统的这种功能是通过检查窗体标题来实现的,因此只要我们隐藏窗体的标题,就能做到在“运行的程序中”隐藏一个窗体。实现的方法很简单,当构造一个新的窗体时,隐藏掉原窗体的标题即可。
这时也有两种方法,一是在原窗体中隐藏和恢复窗体标题:
// Form1
private void btnOK_Click(object sender, EventArgs e)
{
string title = this.Text; // 暂存原窗体标题
Form2 f = new Form2();
f.UserName = m_userName;
this.Text = String.Empty; // 隐藏窗体标题
f.ShowDialog(); // 显示新的窗体
this.Text = title; // 恢复窗体标题
m_userName = f.UserName;
f.Dispose();
}
另外一种方法就是在新窗体中完成原窗体的隐藏和恢复:
// Form1
private void btnOK_Click(object sender, EventArgs e)
{
Form2 f = new Form2(this); // 把原窗体传递给新窗体
f.UserName = m_userName;
f.ShowDialog();
m_userName = f.UserName;
f.Dispose();
}
// Form2
class Form2 : Form
{
private Form m_prevForm; // 用来存放原窗体
private string m_prevFormTitle; // 用来暂存原窗体标题
// 取消默认构造器,换一个带参数的
public Form2(Form prevForm)
{
// 暂存原窗体及其标题
m_prevForm = prevForm;
m_prevFormTitle = prevForm.Text;
// 隐藏原窗体标题
prevForm.Text = String.Empty;
// 添加Closing事件处理器,用来完成恢复动作
this.Closing += new CancelEventHandler(m_onClosing);
// 执行其他初始化
InitializeCompenents();
...
}
// 处理Closing事件,恢复原窗体标题
private void m_onClosing(object sender, CancelEventArgs e)
{
m_prevForm.Text = m_prevFormTitle;
}
}
很明显,第一方方法要简洁得多,而且不需要添加私有域,仅使用一个局部变量即可完成。同时,这种方法还可以避免一些问题,比如当新窗体异常关闭时(如用户强行中止了该窗体等),可能触发不到Closing事件。这种方法还可以避免窗体标题的丢失。
但第一种方法仅适用于使用ShowDialog方法的情况,如果用Show方法启动一个新的窗体,就不得不使用第二种方法,并对异常情况要做更多的考虑了。
注意,尽管Smartphone设备上的Mobile系统不提供察看当前运行的程序的功能,但仍有很多第三方软件可以完成这一功能,因此也必须考虑这种情况。
本文针对在窗体间进行切换时可以使用的一些方法和应该考虑的问题。值得注意的是,一定把窗体导航关系的设计作为整个设计过程中重要的一步来对待;另外一定要在使用完一个窗体后立即调用其Dispose方法释放其占用的资源。