• AvalonDock使用心得


      桌面程序的应用,不可避免的就会用到大量的布局控件,之前的一个项目也想过去做类似于Visual Studio的那种灵活的布局控件,也就是界面上的控件能够实现拖拽放置、隐藏、窗口化等一系列的操作,但由于开发时间以及需求的原因,没有太严格要求这方面功能的实现,也就只能算是想过一下而已,实际用的时候还是固定布局,但是最近接触到新的项目,需要这方面的应用就不得不自己动手查找和做这样的东西了。

      有朋友推荐RadControls里了控件——RadDocking,下载安装RadControls后,发现他里边的控件的确做的很不错,而且Demo也很详细,RadDocking也能满足我的需求,使用也还算方便,但是因为是试用版的,每次程序运行时都会出现相应的提示,尝试找他的最新版的破解版最终也无果,个人又不屑于用很久之前的版本,而且毕竟不是知根知底的东西,用起来也觉得怪怪的,所以还是放弃了使用RadDocking。

      就在我快要放弃寻找,准备有时间自己做的时候(后来发现自己想的有点简单了),让我发现了AvalonDock,看了下它的Demo发现效果也不错,至少看起来能够满足我的应用需求,而且还是开源的,顺便就当研究学习了。大家可以到http://avalondock.codeplex.com/下载和了解更加详细的信息。说了这么多废话赶紧进入正题吧!

      

      虽然有现成的Demo,但第一次接触这类控件还是折腾了不少时间,一点点的摸索它的使用方法!

      一、最基本的布局格式,容器的承载:

    容器布局
         <AvalonDock:DockingManager x:Name="dockManager" Grid.Row="2" Margin="0,3,0,0">
    <AvalonDock:ResizingPanel>
    <AvalonDock:ResizingPanel Orientation="Vertical" AvalonDock:ResizingPanel.ResizeWidth="0.2*">
    <AvalonDock:DockablePane AvalonDock:ResizingPanel.ResizeWidth="0.1*">
    <AvalonDock:DockableContent x:Name="CameraContent" Title="摄像机" FontFamily="微软雅黑" FloatingWindowSize="250,300">
    <VideoMonitor:CameraControl/>
    </AvalonDock:DockableContent>
    </AvalonDock:DockablePane>
    <AvalonDock:DockablePane>
    <AvalonDock:DockableContent x:Name="PTZControlContent" Title="云台控制" FontFamily="微软雅黑">
    <VideoMonitor:PTZControllerControl/>
    </AvalonDock:DockableContent>
    </AvalonDock:DockablePane>
    <AvalonDock:DockablePane AvalonDock:ResizingPanel.ResizeHeight="*">
    <AvalonDock:DockableContent x:Name="PlayOperateContent" Title="回放控制" FontFamily="微软雅黑">
    <VideoMonitor:PlayOperateControl/>
    </AvalonDock:DockableContent>
    </AvalonDock:DockablePane>
    </AvalonDock:ResizingPanel>

    <AvalonDock:ResizingPanel x:Name="VideoResizingPanel">
    <AvalonDock:DocumentPane>
    <AvalonDock:DocumentContent x:Name="VideoBroswerContent" Title="视频监控" FontFamily="微软雅黑">
    <VideoMonitor:VideoBroswerControl x:Name="VideoBroswer"/>
    </AvalonDock:DocumentContent>
    </AvalonDock:DocumentPane>
    </AvalonDock:ResizingPanel>
    </AvalonDock:ResizingPanel>
    </AvalonDock:DockingManager>

      仔细看的话就能发现这里边有一定的层次关系。 

      首先需要一个DockingManager来统筹全局,它能够帮忙管理和处理在其范围内的子级控件的一系列操作——安排载窗格,窗格和处理飞出浮动窗口,以及布局的保存和恢复。感觉好像是只有同一DockingManager下的各个控件才能互相作用,不同DockingManager下的控件是无法跨界操作的。

      再就是DockingManager里放置ResizingPanel,它也是一个容器,用来控制其子控件的布局方式,其Orientation属性类似于StackPanel的同名属性,表示其子级控件是水平或是垂直放置。

      接下来就是两组东西了,它们是一一对应的,DockablePane与DockableContent对应,DocumentPane与DocumentContent对应,而且都是前者包含后者。DockablePane、DocumentPane都可以也都应该放置到ResizingPanel,具体的布局方式就看实际应用的需要了,而DockableContent、DocumentContent下包含的就是我们最终想要呈现给用户的功能模块控件了。

      需要指出的是DockablePane和DocumentPane都继承至Pane,DockableContent和entContent都继承至Managedcontent,在后面一些问题的处理上会用的到。

      下面就分别是DockablePane和DocumentPane的呈现形式,从界面上也能看出点不同的哈,具体的一些不同会在下面根据我自己的经验详细讲解到。

          

     

       接下来就是一些列针对布局的处理了。

       二、布局的保存与恢复

      这两部操作其实很简单,因为DockingManager自身就封装好了相应的方法——SaveLayout、RestoreLayout。它们均有不同的重载形式,即可以传入不同的参数,其中以文件名作为参数传入是最方便的一种。

      实际应用中,需要用户登录时列举出已有的布局列表供其选择,根据其选择应用相应的布局,暂时是通过查找应用程序目录下的xml文件来实现的,就是将该目录下所有的xml文件都列举出来,为此写了一个通用的方法,给定目录和查找的文件扩展名—>返回相应的文件列表。

       

    读取路径下的指定扩展名的文件
    public static List<string> CheckDirectory(string path, string extension)
    {
    List
    <string> xmlpaths = new List<string>();
    try
    {
    if (!File.Exists(path))
    {
    if (Directory.Exists(path))
    {
    string[] paths = Directory.GetFiles(path); //全路径

    foreach (string str in paths)
    {
    if (Path.GetExtension(str) == extension)
    xmlpaths.Add(Path.GetFileNameWithoutExtension(str));
    }
    }
    }
    else
    return null;
    }
    catch (System.Exception /*e*/ )
    {
    return null;
    }

    return xmlpaths;
    }

      程序有个登陆窗口,需要用户选择相应的布局,根据用户的选择应用对应的布局。列表、集合与界面之间都是通过绑定来实现的,下面很多地方都是类似的用法。 这部分倒没什么值得注意的地方,有一点可说的就是要注意区分默认布局和其他布局的处理。

      登陆时的的布局部分
        //登陆窗口
        private List<SelectionItem> layoutlist = new List<SelectionItem>();
        
    public void LayoutListInit()
    {
    SelectionItem item
    = new SelectionItem() { Name = "默认布局" };
    item.IsEnabled
    = false;
    layoutlist.Add(item);

    List
    <string> names = GlobalMethod.CheckDirectory(AppDomain.CurrentDomain.BaseDirectory, ".xml");
    foreach (string str in names)
    {
    if (str != "SampleLayout")
    layoutlist.Add(
    new SelectionItem() { Name = str });
    }
    }

        
    private void OKButton_Click(object sender, System.Windows.RoutedEventArgs e)
    {
    if (operate.HCNet_ServerLoad(ipcombo.Text, servertypecombo.Text,
    porttext.Text, namecombo.Text, passwordbox.Password,MainWindow.handle))
    {
    GlobalData.LayoutList
    = layoutlist;
    this.DialogResult = true;
    this.Close();
    }
    }
     
    //主窗口
        void dockManager_Loaded(object sender, RoutedEventArgs e)
    {
    foreach (SelectionItem item in GlobalData.LayoutList)
    {
    if (item.IsSelected)
    RestoreLayout(item.Name);
    }
    }

    public static void RestoreLayout(string layoutname)
    {
    MainWindow win
    = App.Current.MainWindow as MainWindow;
    if (layoutname == "默认布局")
    {
    if (File.Exists(LayoutFileName))
    win.dockManager.RestoreLayout(LayoutFileName);
    }
    else
    {
    string filename = layoutname + ".xml";
    if (File.Exists(filename))
    win.dockManager.RestoreLayout(filename);
    }
    }

      登陆以后在作了以下处理:

    界面部分 
    <DataTemplate x:Key="LayoutNameListDataTemplate">
       <RadioButton x:Name="radioButton" Margin="0,3,0,0" Content="{Binding Name, Mode=Default}" GroupName="LayoutNameList" VerticalContentAlignment="Top" Checked="layoutchose_Checked" IsChecked="{Binding IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
    </DataTemplate>
     <Style x:Key="MoreWindowsListBoxItemStyle" TargetType="{x:Type ListBoxItem}">
       <Setter Property="Background" Value="Transparent"/>
       <Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
       <Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
       <Setter Property="Padding" Value="2,0,0,0"/>
       <Setter Property="Template">
        <Setter.Value>
         <ControlTemplate TargetType="{x:Type ListBoxItem}">
          <MenuItem Header="{Binding Name, Mode=Default}" IsCheckable="True" IsChecked="{Binding IsSelected, Mode=TwoWay, UpdateSourceTrigger=Default}"/>
          <ControlTemplate.Triggers>
           <Trigger Property="IsSelected" Value="true">
            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}"/>
           </Trigger>
           <MultiTrigger>
            <MultiTrigger.Conditions>
             <Condition Property="IsSelected" Value="true"/>
             <Condition Property="Selector.IsSelectionActive" Value="false"/>
            </MultiTrigger.Conditions>
            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
           </MultiTrigger>
           <Trigger Property="IsEnabled" Value="false">
            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
           </Trigger>
          </ControlTemplate.Triggers>
         </ControlTemplate>
        </Setter.Value>
       </Setter>
      </Style>
    <ListBox ItemsSource="{Binding LayoutList, ElementName=userControl, Mode=Default}" ItemTemplate="{DynamicResource LayoutNameListDataTemplate}"/>

    <ListBox ItemsSource="{Binding DockableContentList, ElementName=userControl, Mode=Default}" ItemContainerStyle="{DynamicResource MoreWindowsListBoxItemStyle}" MenuItem.Checked="winchange_Checked" MenuItem.Unchecked="winchang_Unchecked"/>

    初始化布局列表及控件列表
    private static ObservableCollection<SelectionItem> layoutlist = new ObservableCollection<SelectionItem>();
        private static ObservableCollection<SelectionItem> dockablecontentlist = new ObservableCollection<SelectionItem>();
           private static ObservableCollection<SelectionItem> documentcontentlist = new ObservableCollection<SelectionItem>();
      
        private void LayoutListInit()
    {
           //GlobalData.LayoutList为调用之前的方法获得的布局文件列表

    foreach (SelectionItem item in GlobalData.LayoutList)
    {
    if (item.Name != "SampleLayout")
    layoutlist.Add(item);
    }
    }
      
        private void ContentListInit()
    {
    foreach (DockableContent content in win.dockManager.DockableContents)
    {
    SelectionItem item
    = new SelectionItem() { Name = content.Name };
    if (!(content.State == DockableContentState.Hidden))
    item.IsSelected
    = true;
    dockablecontentlist.Add(item);

    content.StateChanged
    += new RoutedEventHandler(dokablecontent_StateChanged);
    }

    foreach (DocumentContent content in win.dockManager.Documents)
    {
    SelectionItem item
    = new SelectionItem()
    {
    Name
    = content.Name ,
    IsSelected
    = true
    };
    documentcontentlist.Add(item);

    VideoBroswerControl vbcontrol
    = content.Content as VideoBroswerControl;
    if (vbcontrol != null)
    VideoBroswerControl.VideoBroswer
    = vbcontrol;

    content.Closed
    += new EventHandler(content_Closed);
    content.Closing
    += new EventHandler<System.ComponentModel.CancelEventArgs>(content_Closing);
    }

    foreach (FloatingWindow content in win.dockManager.FloatingWindows)
    {
    SelectionItem item
    = new SelectionItem()
    {
    Name
    = content.Name,
    IsSelected
    = true
    };
    dockablecontentlist.Add(item);

    content.Closed
    += new EventHandler(content_Closed);
    }
    }

    public void content_Closing(object sender, System.ComponentModel.CancelEventArgs e)
    {
    MessageBoxResult result
    = MessageBox.Show("窗口即将关闭,该操作将导致所有视频关闭,是否继续?", "", MessageBoxButton.YesNo);
    if (result == MessageBoxResult.No)
    {
    ManagedContent content
    = sender as ManagedContent;
    foreach (SelectionItem item in documentcontentlist)
    {
    if (item.Name == content.Name)
    item.IsSelected
    = true;
    }
    e.Cancel
    = true;
    }
    }

    public void dokablecontent_StateChanged(object sender, RoutedEventArgs e)
    {
    DockableContent content
    = sender as DockableContent;
    if(content.State == DockableContentState.Hidden)
    {
    foreach (SelectionItem item in dockablecontentlist)
    {
    if (item.Name == content.Name)
    item.IsSelected
    = false;
    }
    }
    }

    public void content_Closed(object sender, EventArgs e)
    {
    ManagedContent content
    = sender as ManagedContent;

    foreach (SelectionItem item in documentcontentlist)
    {
    if (item.Name == content.Name)
    item.IsSelected
    = false;
    }
    }
    #endregion

        private void winchange_Checked(object sender, System.Windows.RoutedEventArgs e)
            {
                MenuItem item = e.OriginalSource as MenuItem;
                SelectionItem slitem = item.DataContext as SelectionItem;

                if (slitem != null)
                {
                    DockableContent dockablecontent = win.FindName(slitem.Name) as DockableContent;
                    if (dockablecontent != null)
                    {
                        if (dockablecontent.State == DockableContentState.Hidden)
                            dockablecontent.Show();
                        return;
                    }

                    ManagedContent managecontent = win.FindName(slitem.Name) as ManagedContent;
                    if (managecontent != null)
                    {
                        managecontent.Show();
                        return;
                    }
                }
            }

            private void winchang_Unchecked(object sender, System.Windows.RoutedEventArgs e)
            {
                MenuItem item = e.OriginalSource as MenuItem;
                SelectionItem slitem = item.DataContext as SelectionItem;

                if (slitem != null)
                {
                    ManagedContent content = win.FindName(slitem.Name) as ManagedContent;
                    if (content != null)
                    {
                        content.Hide();
                    }
                }
            }

      也是通过绑定集合的方式与界面结合起来。 由于布局列表在其他地方也用得到,还涉及到添加、删除等操作,为了保证界面与数据的实时响应,使用ObservableCollection<>集合来用于绑定。

                    content.StateChanged += new RoutedEventHandler(dokablecontent_StateChanged);
           content.Closed += new EventHandler(content_Closed);
                    content.Closing += new EventHandler<System.ComponentModel.CancelEventArgs>(content_Closing);
                    content.Closed += new EventHandler(content_Closed);
      以上几个事件尤其需要注意,鼠标对界面操作都是通过相应的事件来与列表之间相互响应的。同时空间的显示与隐藏的实现也在这段代码里。

      保存布局时我就采用的是让用户输入布局名称,根据该名称来保存布局。同时向布局列表中添加该项。   

    保存布局
       private void ok_Click(object sender, System.Windows.RoutedEventArgs e)
       {
    MainWindow win
    = App.Current.MainWindow as MainWindow;
    win.dockManager.SaveLayout(layoutname.Text
    + ".xml");
    win.toolBar.LayoutList.Add(
    new SelectionItem() { Name = layoutname.Text });
    this.Close();
      }

      布局管理中,对已有的布局进行删除操作,删除列表中的项同时删除相应的文件。 

    删除布局
    private void delect_Click(object sender, System.Windows.RoutedEventArgs e)
    {
    if (layoutList.SelectedItems.Count == 0)
    MessageBox.Show(
    "当前没有选中任何布局!");
    else
    {
    MessageBoxResult result
    = MessageBox.Show("是否删除选中的布局?", "", MessageBoxButton.YesNo);

    if (result == MessageBoxResult.Yes)
    {
    while (layoutList.SelectedItems.Count != 0)
    {
    System.IO.File.Delete(layoutList.SelectedItems[
    0].ToString() + ".xml");
    ToolBarControl tbcontrol
    = new ToolBarControl();
    tbcontrol.LayoutList.Remove((SelectionItem)layoutList.SelectedItems[
    0]);
    }
    }
    }
    }

      这样做可能可能不够完善,xml文件的查找就是一个问题,以后考虑通过读取xml文件内容来判断是否是布局文件,暂时还没有想到更好的办法,不知道大家有没有更好的经验呢?!

     四、动态添加控件 

    添加控件
        private void ok_Click(object sender, System.Windows.RoutedEventArgs e)
    {
    if (!GlobalMethod.TestString(videowinname.Text))
    {
    MessageBox.Show(
    "名称只能是字母和数字以及下划线,且不能以数字开头!");
    videowinname.Text
    = "";
    return;
    }

    VideoBroswerControl vbcontrol
    = new VideoBroswerControl();
    vbcontrol.LayoutChanged(VideoBroswerControl.layoutnum);

    DocumentContent documentContent
    = new DocumentContent()
    {
    Name
    = videowinname.Text,
    Title
    = videowinname.Text,
    Content
    = vbcontrol
    };
    MainWindow win
    = App.Current.MainWindow as MainWindow;
    win.RegisterName(videowinname.Text, documentContent);
    documentContent.Show(win.dockManager);

    ToolBarControl tlcontrol
    = new ToolBarControl();
    SelectionItem item
    = new SelectionItem()
    {
    Name
    = videowinname.Text ,
    IsSelected
    =true
    };
    tlcontrol.DocumentContentList.Add(item);
    documentContent.Closed
    += new EventHandler(tlcontrol.content_Closed);
    documentContent.Closing
    += new EventHandler<System.ComponentModel.CancelEventArgs>(tlcontrol.content_Closing);
    this.Close();
    }

    五、其他

      还有这样一个事件时的注意的,其实我也说不好他的本质是什么,感觉好像就是每次启动新的布局时,如果以有布局存在空缺或已经关闭的情况下就会到达这里,所以在我在这里将缺失的控件给加上。

    代码
          dockManager.DeserializationCallback += (s, e) =>
    {
    DockingManager manager
    = s as DockingManager;

    VideoBroswerControl vbcontrol
    = new VideoBroswerControl();
    vbcontrol.LayoutChanged(VideoBroswerControl.layoutnum);

    var documentContent
    = new DocumentContent()
    {
    Name
    = e.Name,
    Title
    = e.Name,
    Content
    = vbcontrol
    };

    e.Content
    = documentContent;
    };

    呵呵,一点小小经验,文章也拖了好久才写好,大家见笑啦!

    偷懒了,没有写一个更好的Demo给大家,用了一些自己项目里现成的代码,希望有问题的朋友可以多多交流,也请大家多多指教,赐教我一些更好的方法!

  • 相关阅读:
    【Azure 应用服务】Azure Function在执行Function的时候,如果失败了,是否可以重试呢?
    【Azure Developer】使用 Azure Python 查看 Azure 所有的 Alert rule
    【Azure 环境】使用Microsoft Graph PS SDK 登录到中国区Azure, 命令ConnectMgGraph Environment China xxxxxxxxx 遇见登录错误
    【Azure Fabric Service】怎样关闭 Azure Service Fabric?
    【Azure 事件中心】关闭或开启Azure Event Hub SDK中的日志输出
    【Azure 应用服务】App Service运行时突然中断:There is not enough space on the disk : 'D:localTempASPNETCORE...
    【Azure 应用服务】App Service for Linux环境中,如何解决字体文件缺失的情况
    【Azure Developer】使用 CURL 获取 Key Vault 中 Secrets 中的值
    用SQL命令查看Mysql数据库大小 统计数据库空间占用
    各类手册Manual在线地址分享(AutoHotKey、Bash、lftp、sphinx、PHP、MySQL、Python、Perl、Vim等等...)
  • 原文地址:https://www.cnblogs.com/wdysunflower/p/1779960.html
Copyright © 2020-2023  润新知