创建带标签页的MDI WinForms应用程序
http://www.cnblogs.com/island/archive/2008/12/02/mditab.html
创建MDI应用程序
- 先创建”Windows窗体应用程序”解决方案TabableMDIApp.
- 选中TabableMDIApp项目,添加新建项“MDI父窗体”:TabableMDIApp。VS自动创建的MDI父窗体已经帮我们做好了一个标准MDI应用程序所需要的大多数操作,而我们需要做的就是点点鼠标、喝喝茶、听听音乐、吹吹牛。
- 将Program.cs中的Form1改为TabableMDIApp,这样不出意外的话编译就应该可以通过了。这样只花了几秒钟的时间就创建了一个基本的MDI应用程序——框架。效果如下:
添加标签页管理功能
不过我们需要的是一个像Firefox那样不仅具有多窗体而且还具有相应的子窗体标签页管理控件。所以还需要进行如下操作:
- 在工具箱中拖一个TabControl到TabableMDIApp窗体中可将其命名为“TabPageManager”,并移除VS默认添加的tabPage1和tabPage2标签页(清空TabControl的TabPages属性中的标签页集合),设置TabControl的Dock属性为Top。
- 然后创建子窗体模板:修改VS自动添加的Form1窗体:将其文件重命名为RichTextChildTabForm,同时将Form1类重构为RichTextChildTabForm。
- 在RichTextChildTabForm.cs中添加两个属性,分别用于存储父窗体的TabControl对象和对应的子标签页,如下所示:
private TabControl tabPageManager;
public TabControl TabPageManager {
set { tabPageManager = value; }
}
private TabPage tabPageChild;
public TabPage TabPageChild {
get { return tabPageChild; }
set { tabPageChild = value; }
}
4. 添加子窗体关闭处理代码:
// 当子窗体关闭时销毁相应的TabPage
private void RichTextChildTabForm_FormClosing( object sender, FormClosingEventArgs e ) {
this.tabPageChild.Dispose();
// 如果没有TabPage对象则隐藏TabControl
if(!tabPageManager.HasChildren ) {
tabPageManager.Visible = false;
}
}
5. 当子窗体激活的时候激活相应的标签页:
// 子窗体激活时激活相应的标签页
private void RichTextChildTabForm_Activated( object sender, EventArgs e ) {
tabPageManager.SelectedTab = tabPageChild;
if(! tabPageManager.Visible ) {
tabPageManager.Visible = true;
}
}
6. 当MDI父窗体中“新建窗口”菜单被点击新建子窗体时,为新建窗体的TabPageManager和TabPageChild属性设置相应值:
private void ShowNewForm( object sender, EventArgs e ) {
//Creating MDI child form and initialize its fields
RichTextChildTabForm childForm = new RichTextChildTabForm();
childForm.Text = "窗口 " + childFormNumber++;
childForm.MdiParent = this;
//child Form will now hold a reference value to the tab control
childForm.TabPageManager = this.tabPageManager;
//Add a Tabpage and enables it
TabPage tp = new TabPage();
tp.Parent = tabPageManager;
tp.Text = childForm.Text;
tp.Show();
//child Form will now hold a reference value to a tabpage
childForm.TabPageChild= tp;
//Activate the MDI child form
childForm.Show();
//Activate the newly created Tabpage
tabPageManager.SelectedTab = tp;
}
7. 当父窗体的TabControl上被选中的标签页索引发生变化时激活相应的标签页:
private void tabPageManager_SelectedIndexChanged( object sender, EventArgs e ) {
foreach( RichTextChildTabForm childForm in this.MdiChildren ) {
//Check for its corresponding MDI child form
if( childForm.TabPageChild.Equals(tabPageManager.SelectedTab ) ) {
//Activate the MDI child form
childForm.Select();
}
}
}
这样一个带标签页的MDI窗体应用程序就创建好了,运行效果如下:
在多个标签页中分别打开多个文件
在UltraEdit里面我们可以同时选中多个文本文件然后程序会自动在各个标签页中加载相应文件,在其他MDI应用程序中也会碰到类似现象下面我们可以按如下步骤实现上述功能:
- 先在RichTextChildTabForm中添加RichTextBox控件以用于加载显示我们将要选中的文本文件
- 设置父窗体的打开文件对话框:openFileDialog.Multiselect = true;以允许我们同时选中多个文件。同时打开多个选中文本代码如下:
private void OpenFile( object sender, EventArgs e ) {
OpenFileDialog openFileDialog = new OpenFileDialog();
//openFileDialog.InitialDirectory = Environment.GetFolderPath( Environment.SpecialFolder.Personal );
openFileDialog.Multiselect = true;
openFileDialog.Filter = "文本文件(*.txt)|*.txt|所有文件(*.*)|*.*";
if( openFileDialog.ShowDialog( this ) == DialogResult.OK ) {
string[] FileNames = openFileDialog.FileNames;
OpenSelectedFiles(FileNames);
}
}
private void OpenSelectedFiles( string[] FileNames ) {
RichTextChildTabForm childForm;
foreach( string fileToOpenItem in FileNames ) {
//Creating MDI child form and initialize its fields
childForm = new RichTextChildTabForm();
childFormNumber++;
childForm.Text = fileToOpenItem.Substring( fileToOpenItem.LastIndexOf( @"\" ) + 1 );
childForm.MdiParent = this;
//child Form will now hold a reference value to the tab control
childForm.TabPageManager = this.tabPageManager;
//Add a Tabpage and enables it
TabPage tp = new TabPage();
tp.Parent = tabPageManager;
tp.Text = childForm.Text;
tp.Show();
//child Form will now hold a reference value to a tabpage
childForm.TabPageChild = tp;
childForm._RichTextBox.LoadFile( fileToOpenItem, RichTextBoxStreamType.PlainText );
//Activate the MDI child form
childForm.Show();
//Activate the newly created Tabpage
tabPageManager.SelectedTab = tp;
}
}
不过又有新的问题了:如果我们重复打开同一个文件怎么办?比如我先前已经打开了C:\A.txt、C:\B.txt、C:\C.txt、C:\D.txt、C:\E.txt后来我出去溜达一圈忘了打开过什么了结果我又打开了C:\A.txt这又该如何呢?所以我们希望每一个标签页都知道自己打开了什么文件,可以在子标签页中添加一个属性:
private string openedFileNameOfThisTab;
public string OpenedFileNameOfThisTab {
get { return openedFileNameOfThisTab; }
set { openedFileNameOfThisTab = value; }
}
,然后父窗体要能记住当前已经打开过的文件列表以及相应文件是在哪个标签页中打开的可以在父窗体中添加以下代码:
private static Hashtable openedFileHashtable = new Hashtable();
public static Hashtable OpenedFileHashtable {
get { return DispenserMDIParent.openedFileHashtable; }
}
同时我们将上面的文件打开方法定义为如下形式:
private void OpenSelectedFiles( string[] FileNames ) {
RichTextChildTabForm childForm;
foreach( string fileToOpenItem in FileNames ) {
// 如果该文件未被打开(不在打开文件哈希表中),则在新标签页中打开此文件
if( !openedFileHashtable.Contains( fileToOpenItem ) ) {
// 哈希表的键为打开文件的绝对路径含文件名,值为子窗口数即将要打开此文件的标签页索引:IndexNumber
openedFileHashtable.Add( fileToOpenItem, this.MdiChildren.Length );
//Creating MDI child form and initialize its fields
childForm = new RichTextChildTabForm();
childFormNumber++;
childForm.Text = fileToOpenItem.Substring( fileToOpenItem.LastIndexOf( @"\" ) + 1 );
childForm._RichTextBox.LoadFile(fileToOpenItem,RichTextBoxStreamType.PlainText);
childForm.OpenedFileNameOfThisTab = fileToOpenItem;
AddorActiveTabPage( childForm, true );
} else {
// 如果该文件已经被打开(在打开文件哈希表中),则激活相应标签页
// 哈希表中与文件名关联的值为此文件相关联的标签索引
tabPageManager.SelectedIndex = (int)openedFileHashtable[fileToOpenItem];
}
}
}
/// <summary>
/// 在MDI窗体中添加新标签页或者激活已经存在的标签页,true,则添加,false则激活
/// </summary>
/// <param name="childForm">待添加或者激活的TabableForm对象实例</param>
/// <param name="addPage">true,则添加,false则激活已存在标签页</param>
private void AddorActiveTabPage( RichTextChildTabForm childForm, bool addPage ) {
if( addPage ) {
childForm.MdiParent = this;
//child Form will now hold a reference value to the tab control
childForm.TabPageManager = tabPageManager;
//Add a Tabpage and enables it
TabPage tp = new TabPage();
tp.Parent = tabPageManager;
tp.Text = childForm.Text;
tp.Show();
//child Form will now hold a reference value to a tabpage
childForm.TabPageChild = tp;
//Activate the MDI child form
childForm.Show();
childForm.Activate();
//Activate the newly created Tabpage
tabPageManager.SelectedTab = tp;
} else {
//childForm.Show();
//childForm.Activate();
//TabableForm tpForm = this.ActiveMdiChild as TabableForm;
//this.viewerTabManager.SelectedTab = tpForm.ChildTabPage;
childForm.Select();
}
}
记住在“关闭所有窗口”时清空文件打开列表:
private void CloseAllToolStripMenuItem_Click( object sender, EventArgs e ) {
foreach( Form childForm in MdiChildren ) {
childForm.Close();
}
openedFileHashtable.Clear();
}
并且在子窗体关闭时删除文件打开列表中与该标签页对应的文件如下所示:
// 当子窗体关闭时销毁相应的TabPage,并删除其在父窗体打开文件列表中的相应项
private void RichTextChildTabForm_FormClosing( object sender, FormClosingEventArgs e ) {
if( !string.IsNullOrEmpty( this.OpenedFileNameOfThisTab ) ) {
// 关闭标签页时删除打开文件列表中的相应文件名
TabableMDIApp.OpenedFileHashtable.Remove( this.OpenedFileNameOfThisTab );
}
this.tabPageChild.Dispose();
// 如果没有TabPage对象则隐藏TabControl
if(!tabPageManager.HasChildren ) {
tabPageManager.Visible = false;
}
}
关闭标签页
现在我们可以通过两种方式关闭已经打开的标签页——直接关闭所有窗口或者,点击子窗口的关闭按钮,不过我们希望以一种更简单的方式关闭标签页——双击标签页则将其关闭,这也是GreenBrowser浏览器采取的方案。可以添加如下代码实现:
private void tabPageManager_MouseDoubleClick( object sender, MouseEventArgs e ) {
Form childForm = this.ActiveMdiChild;
RichTextChildTabForm tabableChild = childForm as RichTextChildTabForm;
if( tabableChild != null && !string.IsNullOrEmpty( tabableChild.OpenedFileNameOfThisTab ) ) {
// 如果 Hashtable 不包含带有指定键的元素,则 Hashtable 保持不变。不引发异常。
// 此方法的运算复杂度是 O(1)。
openedFileHashtable.Remove( tabableChild.OpenedFileNameOfThisTab );
}
//Destroy the corresponding Tabpage when closing MDI child form
// 关闭当前标签页后自动激活前一个标签页
int preTabIndex = tabPageManager.SelectedIndex - 1;
this.tabPageManager.SelectedTab.Dispose();
if( childForm != null ) {
childForm.Dispose();
}
//If no Tabpage left
if( !tabPageManager.HasChildren ) {
tabPageManager.Visible = false;
} else {
tabPageManager.SelectedIndex = preTabIndex;
}
}
拖放打开多个文件
那么如何让用户可以通过将文件拖放到应用程序窗口的方式打开呢?在默认情况下当用户将文件拖放到程序窗口中的时侯鼠标呈现出“禁止”的形状。如下:
要想支持拖放式打开可以按如下步骤操作:
- 设置“TabableMDIApp”这个Form的AllowDrop属性为True;
- 为“TabableMDIApp”这个Form添加DragEnter事件处理方法:
// This event occurs when the user drags over the form with
// the mouse during a drag drop operation
private void TabableMDIApp_DragEnter( object sender, DragEventArgs e ) {
// Check if the Dataformat of the data can be accepted
// (we only accept file drops from Explorer, etc.)
if( e.Data.GetDataPresent( DataFormats.FileDrop ) )
e.Effect = DragDropEffects.Link; //.Copy Okay
else
e.Effect = DragDropEffects.None; // Unknown data, ignore it
}
- 为“TabableMDIApp”这个Form添加DragDrop事件处理方法:
/// <summary>
/// 支持用户拖放文件并将其打开操作
/// </summary>
private void TabableMDIApp_DragDrop( object sender, DragEventArgs e ) {
if( e.Data.GetDataPresent( DataFormats.FileDrop ) ) {
// 用户拖放文件列表
System.Array fileNames = (System.Array)e.Data.GetData( DataFormats.FileDrop );
// 取得用户拖放的所有文件的文件名
string[] dropedFileNames=new string[fileNames.Length];
for( int i = 0; i < fileNames.Length; i++ ) {
dropedFileNames[i] = fileNames.GetValue( i ).ToString();
}
// 在新标签页中打开拖放的文件,如果支持该格式的话
OpenSelectedFiles( dropedFileNames );
}
}
打开异构标签页
通常标签页承载的都是同一个对象的多个实例,也就是说从表面上看来这多个标签页中的基本控件都是一样的,比如UltraEdit的多个标签页只是打开不同的文本而已,实际上标签页里面都是一个编辑器,本人称之为“同构”的,不过Visual Studio 的标签也就不是“同构”的:他可以在一个标签页中编辑源代码,而在另一个标签页中配置项目属性,或者打开一个对象浏览器,而这些标签页都包含着不同的控件,姑且称之为“异构”的,这又是如何实现的呢?
其实也不难,我们可以创建一个BaseTabForm窗体类,这个类只包含MDI子窗体的一些基本属性、字段或者方法,然后让其余待创建的异构窗体继承这个类,然后在标签页中显示由BaseTableForm继承而来的之类,但是管理标签的时候我们统一将其当作BaseTableForm来对待即可,这其实是一种多态手法。具体做法如下:
新建一个BaseTabForm“Windows窗体“对象,该窗体中不含其他控件,然后将前面所创建的RichTextChildTabForm中除了OpenedFileNameOfThisTab属性和构造函数之外的代码剪切到BaseTabForm中,最后代码大致如下:
using System;
using System.Windows.Forms;
namespace TabableMDIApp {
public partial class BaseTabForm :Form {
private TabControl tabPageManager;
public TabControl TabPageManager {
set { tabPageManager = value; }
}
private TabPage tabPageChild;
public TabPage TabPageChild {
get { return tabPageChild; }
set { tabPageChild = value; }
}
public BaseTabForm() {
InitializeComponent();
}
// 当子窗体关闭时销毁相应的TabPage,并删除其在父窗体打开文件列表中的相应项
private void BaseTabForm_FormClosing( object sender, FormClosingEventArgs e ) {
this.tabPageChild.Dispose();
// 如果没有TabPage对象则隐藏TabControl
if( !tabPageManager.HasChildren ) {
tabPageManager.Visible = false;
}
}
// 子窗体激活时激活相应的标签页
private void BaseTabForm_Activated( object sender, EventArgs e ) {
tabPageManager.SelectedTab = tabPageChild;
if( !tabPageManager.Visible ) {
tabPageManager.Visible = true;
}
}
}
}
using System;
using System.Windows.Forms;
namespace TabableMDIApp {
public partial class RichTextChildTabForm : Form { // 注意这里
private string openedFileNameOfThisTab;
public string OpenedFileNameOfThisTab {
get { return openedFileNameOfThisTab; }
set { openedFileNameOfThisTab = value; }
}
public RichTextChildTabForm() {
InitializeComponent();
}
private void RichTextChildTabForm_FormClosing( object sender, FormClosingEventArgs e ) {
if( !string.IsNullOrEmpty( this.OpenedFileNameOfThisTab ) ) {
// 关闭标签页时删除打开文件列表中的相应文件名
TabableMDIApp.OpenedFileHashtable.Remove( this.OpenedFileNameOfThisTab );
}
}
}
}
注意相关的窗体事件处理委托也要做相应的修改,然后再手工将public partial class RichTextChildTabForm :Form 改为public partial class RichTextChildTabForm :BaseTabForm,从此以后对于任意新创建的子窗体类都可以将其直接继承的Form基类改为BaseTabFrom,这样就可以创建异构的窗体对象而将其当作BaseTabForm对象处理了。
将父窗体中添加或者激活标签的方法的签名改为如下:
private void AddorActiveTabPage( BaseTabForm childForm, bool addPage )
改变标签页方法改为如下:
// 当父窗体的TabControl上被选中的标签页索引发生变化时激活相应的标签页
private void tabPageManager_SelectedIndexChanged( object sender, EventArgs e ) {
foreach(BaseTabForm childForm in this.MdiChildren ) {
//Check for its corresponding MDI child form
if( childForm.TabPageChild.Equals( tabPageManager.SelectedTab ) ) {
//Activate the MDI child form
childForm.Select();
}
}
}
下面我们可以在标签页中添加一个带Button的标签页,其类代码如下:
namespace TabableMDIApp {
public partial class ButtonTabForm :BaseTabForm {
public ButtonTabForm() {
InitializeComponent();
}
}
}
然后可以响应父窗体中添加Button标签页的方法:
private void addButtonTab_Click( object sender, EventArgs e ) {
ButtonTabForm btnTab = new ButtonTabForm();
btnTab.Text = "带按钮的标签页";
AddorActiveTabPage(btnTab,true);
}