65.9K
CodeProject 正在变化。 阅读更多。
Home

Reflection Studio - 第二部分 - 用户界面:主题、对话框、控件、外部库

starIconstarIconstarIconstarIconstarIcon

5.00/5 (31投票s)

2010 年 9 月 3 日

GPL3

13分钟阅读

viewsIcon

99891

downloadIcon

7

Reflection Studio 是一款用于程序集、数据库、性能和代码生成的“开发者”应用程序,使用 C# 和 WPF 4.0 编写。

引言

这是我关于 Reflection Studio 文章的第二部分。在本章中,我将介绍通用的用户界面相关组件:使用皮肤和颜色编写主题,定义对话框模板,用户控件,以及使用 AvalonDock 和 Fluent 等外部程序集来构建主界面。像程序集树视图这样的特定控件将在相关的文章部分讨论。

Reflection Studio 托管在 http://reflectionstudio.codeplex.com/。它完全使用 C# 在 .NET/WPF 平台上编写。我最近迁移到了 Visual Studio 2010 和 NET 4。请查看 CodePlex 项目,因为它太大了,无法详细描述所有内容。这是应用程序的截图

ReflectionStudio

目录

正如之前所说,主题内容非常庞大。我将(尝试)分几部分来写这篇文章

第二部分 - 用户界面

所有通用的用户界面控件都在 Reflection.Studio.Controls 程序集中。下面的类图展示了主要类,我们将在接下来的章节中尝试探讨它们。

2.1 主题:皮肤和颜色

我开始使用 WPF 并观看大量示例时感到困扰的是,定义主题的资源字典总是混合了外观和颜色。通常无法更改“aero”风格的颜色,因为颜色在基于蓝色玻璃颜色的资源字典中被“硬编码”了。更重要的是,如果我决定使用外部库,我将依赖于它内部定义的样式类型(皮肤+颜色)。对于皮肤,没问题!如果实现得好,我可以选择用我自己的来覆盖它。但是颜色呢?如果我想支持银色、蓝色和黑色,但库没有蓝色怎么办?仅仅为了颜色就重写所有皮肤?

在查看了 Fluent 代码后,我发现它更加灵活和合乎逻辑。解决方案是定义皮肤(模板)所需使用的基本或默认颜色资源。Color.xaml 是默认的,模板使用 DynamicResource。添加一个 Color.Blue.xaml,那么你的皮肤就可以拥有不同的颜色。

对于外部程序集,我目前没有简单的解决方案;这就是为什么它们总是与默认皮肤一起使用,无论颜色如何……结果不是很理想!

Helpers 命名空间包含两个用于主题管理的助手,它们满足两种不同的需求,并在下面进行了描述。

2.1.1 - 主题中的固定皮肤和颜色

ThemeHelper 可以发现你嵌入的应用程序主题,并在你定义如下字典时加载它们(在主程序中),但要注意程序集链接和性能问题。这是以前的 Reflection Studio 方法。

  • <Resources>
    • <Themes>
      • <Black>
        • 所有必须包含在下面 black.xaml 文件中的主题字典
      • <Blue>
        • 所有必须包含在下面 blue.xaml 文件中的主题字典
      • Black.xaml
      • Blue.xaml

这是一个使用它的代码示例。填充将在用户界面中显示的“工作区主题”集合

//load workspace values
WorkspaceService.Instance.Themes = ThemeHelper.DiscoverThemes();

响应菜单项点击(包含主题颜色作为 string),并加载主题

private void ThemeMenuItem_Click(object sender, RoutedEventArgs e)
{
    WorkspaceService.Instance.Entity.CurrentTheme =
               (string)((System.Windows.Controls.MenuItem)sender).Header;
    ThemeHelper.LoadTheme(WorkspaceService.Instance.Entity.CurrentTheme);
}

2.1.2 - 用于主题组合的灵活皮肤和颜色

ThemeManager 将根据如下配置加载皮肤和颜色字典。定义了一个 ThemeElement 类来序列化/反序列化配置文件,并用于将资源字典应用到应用程序。它有助于定义带有关联 ResourceDictionary 的颜色和皮肤。

<ThemeElementCollection>
	<!--COLORS-->
	<ThemeElement Group="Colors" Name="Black" IsDefault="true" IsSelected="false" 
	Image="/ReflectionStudio;component/Resources/Images/32x32/color.png">
		<Dictionary>/ReflectionStudio.Controls;component/
			Resources/Colors/Colors.Black.xaml</Dictionary>
		<Dictionary>/Fluent;component/Themes/Office2010/Common.xaml</Dictionary>
		<Dictionary>/Fluent;component/Themes/Office2010/Colors/ColorsBlack.xaml
		</Dictionary>
	</ThemeElement>
	<ThemeElement Group="Colors" Name="Blue" IsDefault="false" IsSelected="false" 
		Image="/ReflectionStudio;component/Resources/Images/32x32/color.png">
		<Dictionary>/ReflectionStudio.Controls;component/
			Resources/Colors/Colors.Blue.xaml</Dictionary>
		<Dictionary>/Fluent;component/Themes/Office2010/Common.xaml</Dictionary>
		<Dictionary>/Fluent;component/Themes/Office2010/
			Colors/ColorsBlue.xaml</Dictionary>
	</ThemeElement>
	<ThemeElement Group="Colors" Name="Silver" IsDefault="false" IsSelected="false" 
		Image="/ReflectionStudio;component/Resources/Images/32x32/color.png">
		<Dictionary>/ReflectionStudio.Controls;component/
			Resources/Colors/Colors.Silver.xaml</Dictionary>
		<Dictionary>/Fluent;component/Themes/Office2010/Common.xaml
			</Dictionary>
		<Dictionary>/Fluent;
			component/Themes/Office2010/Colors/ColorsSilver.xaml
		</Dictionary>
	</ThemeElement>
	<!--SKINS-->
	<ThemeElement Group="Skins" Name="Glossy" IsDefault="true" IsSelected="false" 
		Image="/ReflectionStudio;component/Resources/Images/32x32/skin.png">
		<Dictionary>/AvalonDock;component/themes/generic.xaml</Dictionary>
		<Dictionary>/ReflectionStudio.Controls;component/
			Resources/Skins/Glossy.xaml</Dictionary>
		<Dictionary>/ReflectionStudio;component/
			Resources/Dictionnaries/AvalonDock.Glossy.xaml</Dictionary>
		<Dictionary>/ReflectionStudio;component/Resources/
			AssemblyDesignerItem.xaml</Dictionary>
	</ThemeElement>
	<ThemeElement Group="Skins" Name="Blend" IsDefault="false" IsSelected="false" 
	Image="/ReflectionStudio;component/Resources/Images/32x32/skin_blend.png">
		<Dictionary>/AvalonDock;component/themes/generic.xaml</Dictionary>
		<Dictionary>/ReflectionStudio.Controls;component/
			Resources/Skins/Blend.xaml</Dictionary>
		<Dictionary>/ReflectionStudio;component/
		Resources/Dictionnaries/AvalonDock.Expression.xaml</Dictionary>
		<Dictionary>/ReflectionStudio;component/
			Resources/AssemblyDesignerItem.xaml</Dictionary>
	</ThemeElement>
	<ThemeElement Group="Skins" Name="Visual Studio" IsDefault="false" 
		IsSelected="false" Image="/ReflectionStudio;component/
			Resources/Images/32x32/skin_studio.png">
		<Dictionary>/AvalonDock;component/themes/generic.xaml</Dictionary>
		<Dictionary>/ReflectionStudio.Controls;component/
			Resources/Skins/VisualStudio.xaml</Dictionary>
		<Dictionary>/ReflectionStudio;component/
			Resources/Dictionnaries/AvalonDock.Studio.xaml</Dictionary>
		<Dictionary>/ReflectionStudio;component/
			Resources/AssemblyDesignerItem.xaml</Dictionary>
	</ThemeElement>
</ThemeElementCollection>

要应用主题资源,请使用下面的 LoadThemeResource 函数,它会移除旧资源,添加新资源,并更改配置的 IsSelected 标志,以便以后可以保存/恢复。

更新

  1. 我现在可以覆盖外部程序集字典,并且 Avalon 现在有 3 种颜色,因为我重新定义了它的基本字典
  2. 现在,更改资源字典的操作位于 Application.Current.Resources.BeginInit();Application.Current.Resources.EndInit(); 调用之间,以确保在更改模板时不会实时更新用户界面。
  3. 我在启动时设置了一个特定函数——因为 App.xaml 中包含的资源可能与配置的字典不同(在设计时),所以我会移除所有不存在于初始定义中的内容。
  4. 优化:如果现有已加载的字典存在于新的皮肤或颜色中,我不会移除它们。UnLoadDictionaries 已不再存在。
  5. 副作用:像资源管理器这样的某些控件过去使用 ApplyTemplate 重写来填充它们。这已被修改,因为每次主题更改都会调用此方法,导致例如树中出现多个项目。
  6. 我还添加了一个调试函数,以递归方式跟踪应用程序中加载的所有字典。

下面是将在 Load 方法中调用的 InitializeResource 函数,以及现在接受两个参数(旧资源和新资源定义)的新 LoadDictionaries

/// <summary>
/// Load the specified ThemeElement list and remove old ones
/// </summary>
/// <param name="themeResourceList"></param>
private void InitializeResource(List<ThemeElement> themeResourceList)
{
	Tracer.Verbose("ThemeManager:InitializeResource", "START");

	//TraceDictionnaries();

	try
	{
		Application.Current.Resources.BeginInit();

		//remove old ones
		List<ResourceDictionary> olds = new List<ResourceDictionary>();
		List<string> newsItems = new List<string>();

		foreach (ThemeElement element in themeResourceList)
			newsItems.AddRange(element.Dictionaries);

		foreach (string dico in newsItems)
		{
			//in existing dictionary, be sure to remove old ones, 
			//in particular the on coming from xaml like App.xaml
			foreach (ResourceDictionary dictionnary in 
			Application.Current.Resources.MergedDictionaries)
			{
				if (newsItems.Find(p => p == 
				dictionnary.Source.OriginalString) == null)
					olds.Add(dictionnary);
			}
		}

		foreach (ResourceDictionary dictionnary in olds)
			Application.Current.Resources.MergedDictionaries.Remove
				(dictionnary);

		foreach (ThemeElement element in themeResourceList)
		//add new ones
			LoadDictionaries(element);

		Application.Current.Resources.EndInit();
	}
	[...]

/// <summary>
/// Load the specified ThemeElement in the application and set it as IsSelected
/// </summary>
/// <param name="themeResource"></param>
private void LoadDictionaries
	(ThemeElement oldThemeResource, ThemeElement newThemeResource)
{
	Tracer.Verbose("ThemeManager:LoadDictionaries", "START");
	try
	{
		try
		{
			foreach (string dictionnary in oldThemeResource.Dictionaries)
			{
				ResourceDictionary dic = 
				Application.Current.Resources.MergedDictionaries.
				FirstOrDefault(p => p.Source.OriginalString == 
				dictionnary);
				if (dic != null)
				{
					//does not exist in new ones
					if (newThemeResource.Dictionaries.Where
					(p => p == dic.Source.OriginalString).
						Count() == 0)
						Application.Current.Resources.
						MergedDictionaries.Remove(dic);
				}
			}
			oldThemeResource.IsSelected = false;
		}
		catch (Exception all)
		{
			Tracer.Error("ThemeManager.LoadDictionaries", all);
		}

		foreach (string dictionnary in newThemeResource.Dictionaries)
		{
			//if does not exist in application
			if (Application.Current.Resources.MergedDictionaries.
			Where(p => p.Source.OriginalString == 
				dictionnary).Count() == 0)
			{
				Uri Source = new Uri(dictionnary, UriKind.Relative);
				ResourceDictionary dico = 
				(ResourceDictionary)Application.LoadComponent(Source);
				dico.Source = Source;
				Application.Current.Resources.
					MergedDictionaries.Add(dico);
			}
		}
		newThemeResource.IsSelected = true;
	}
	catch (Exception all)
	{
		Tracer.Error("ThemeManager.LoadDictionaries", all);
	}
	Tracer.Verbose("ThemeManager:LoadDictionaries", "END");
}

然后,您将获得一个资源集合,这些资源可以独立地应用于同一个“颜色”或“皮肤”组。在 Reflection Studio 中,我将整个集合绑定到两个图库和一个过滤器,如下所示。

<Fluent:InRibbonGallery x:Name="inRibbonGallery_Color"
    ItemsSource ="{Binding Themes}"
    ItemTemplate="{StaticResource ColorDataItemTemplate}"
    Text="Colors" GroupBy="Group"
    ResizeMode="Both" MaxItemsInRow="3"
    MinItemsInRow="1" ItemWidth="40"
    ItemHeight="55" ItemsInRow="3"
    SelectionChanged="inRibbonGallery_Color_SelectionChanged">
      <Fluent:InRibbonGallery.Filters>
        <Fluent:GalleryGroupFilter Title="All" Groups="Colors" />
      </Fluent:InRibbonGallery.Filters>
</Fluent:InRibbonGallery>

皮肤和颜色然后显示在主功能区选项卡的“主题”图库中,如下图所示。

下面是“光泽”皮肤与三种颜色的示例。正如您所见,我们需要一些帮助来获得更好的设计……

2.2 对话框:标准、带标题、消息框

我定义了一些模板和一个关联的类,使对话框更符合主 Office 窗口风格的外观和感觉。这意味着圆角边框带阴影、系统按钮……

2.2.1 - WindowBase

由于程序集还包含以前使用的 OfficeWindow(代替 Fluent 窗口),因此有一个通用的窗口类来管理它的大小调整,通过一个抓手(gripper)。如果窗口没有 ResizeMode.CanResizeWithGrip 样式,则抓手不会显示。

public override void OnApplyTemplate()
{
    base.OnApplyTemplate();

    if (!DesignerProperties.GetIsInDesignMode(this))
    {
        if (this.ResizeMode == System.Windows.ResizeMode.CanResizeWithGrip)
        {
            FrameworkElement resizeBottomRight =
                     (FrameworkElement)GetTemplateChild(ResizeGripPART);
            resizeBottomRight.MouseDown += OnResizeRectMouseDown;
            resizeBottomRight.MouseMove += OnResizeRectMouseMove;
            resizeBottomRight.MouseUp += OnResizeRectMouseUp;
        }
        else
        {
            FrameworkElement resizeBottomRight =
                     (FrameworkElement)GetTemplateChild(ResizeGripPART);
            resizeBottomRight.Visibility = System.Windows.Visibility.Hidden;
        }
    }
}

根据样式,我们连接抓手来处理大小调整逻辑,或者隐藏它。对话框不能通过其边框来调整大小,因此我们必须在派生类中管理这一点。OfficeWindow 类和模板现在已超出范围,但请查看代码,它相当有趣。

2.2.2 - Dialog

DialogWindowHeaderedDialogMessageBox 的基类,它们通常在 Reflection Studio 中使用。下面是使用 DialogWindow 作为基类的启动对话框示例,以及一个作为 HeaderedDialog 示例的关于对话框。

XAML 样式非常简单,定义了一个圆角边框、一个按钮和抓手。一些属性已更改,如 AllowsTransparencyWindowStyleBackgroundShowInTaskbar

<!-- DialogWindow Style -->
<Style x:Key="{x:Type ucc:DialogWindow}" TargetType="{x:Type ucc:DialogWindow}">
    <Setter Property="SnapsToDevicePixels" Value="True"/>
    <Setter Property="AllowsTransparency" Value="True"/>
    <Setter Property="WindowStyle" Value="None"/>
    <Setter Property="Background" Value="Transparent"/>
    <Setter Property="ShowInTaskbar" Value="False"/>
    <Setter Property="Template">
        <Setter.Value>
        <ControlTemplate TargetType="{x:Type ucc:DialogWindow}">

            <Grid Margin="10">
            <!--Windows Frame rectangle-->
            <Rectangle Style="{StaticResource RectangleFrame}"/>

            <!--PART_Close is the dialog close button-->
            <Button Style="{StaticResource closeButton}"
                x:Name="PART_Close" Height="11" Width="11"
                HorizontalAlignment="Right"
                Margin="0,9,11,0" VerticalAlignment="Top"
                ToolTip="Close" IsCancel="True"/>

            <!-- PART_ContentPresenter -->
            <ContentPresenter x:Name="PART_ContentPresenter"
               HorizontalAlignment="Stretch"
               VerticalAlignment="Stretch"/>

            <ResizeGrip Grid.Column="0" HorizontalAlignment="Right"
                VerticalAlignment="Bottom"
                Width="17" Height="17"
                Focusable="False" Margin="0,0,8,8"
                x:Name="PART_ResizeGrip" Cursor="SizeNWSE"/>
            </Grid>
        </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

DialogWindow 类定义了一个用于右上方关闭按钮的 PART_Close 模板,我们在 OnApplyTemplate 中连接它。

[TemplatePart(Name = "PART_Close", Type = typeof(Button))]
/// <summary>
/// Find the template part to attach button click event handler
/// </summary>
public override void OnApplyTemplate()
{
    base.OnApplyTemplate();

    if (!DesignerProperties.GetIsInDesignMode(this))
    {
        Button close = this.Template.FindName("PART_Close", this) as Button;

        if (close != null)
            close.Click += new RoutedEventHandler(close_Click);
    }
}

然后我们处理关闭事件以及移动。

/// <summary>
/// Handle the button click event
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void close_Click(object sender, RoutedEventArgs e)
{
    if (HandleCloseAsHide)
        this.Hide();
    else
        this.Close();
}

/// <summary>
/// Handle the move event as for dialog background click
/// </summary>
/// <param name="e"></param>
protected override void OnMouseLeftButtonDown(
          System.Windows.Input.MouseButtonEventArgs e)
{
    base.OnMouseLeftButtonDown(e);
    this.DragMove();
}

要使用它,请定义一个新对话框,并更改 XAML 和代码隐藏,使其继承自 DialogWindow。填充它,您将获得模板和默认行为。

public partial class StartupDlg : DialogWindow
<controls:DialogWindow
    x:Class="ReflectionStudio.Components.Dialogs.Startup.StartupDlg"
    xmlns:controls="clr-namespace:ReflectionStudio.
                     Controls;assembly=ReflectionStudio.Controls"
...>

2.2.3 - HeaderedDialog

HeaderedDialog 继承自 Dialog 类,并在其模板中使用 HeaderControl,因此我们得到如下示例。

该类定义了两个额外的 DependencyPropertyDialogDescriptionDialogImage,类型分别为 stringImageSource。仅此而已。

HeaderedDialog 具有与 DialogWindow 类似的模板,并添加了一个 DialogHeader,该 DialogHeader 通过模板绑定到类的 DependencyProperty。要使用它,请定义一个新对话框,并更改 XAML 和代码隐藏,使其继承自 HeaderedDialog

<!--Header-->
<ucc:DialogHeader Grid.Row="0" x:Name="PART_Header"
    VerticalAlignment="Stretch" HasSeparator="Visible"
    Title="{TemplateBinding Property=Title}"
    Image="{TemplateBinding Property=DialogImage}"
    Description="{TemplateBinding Property=DialogDescription}" />

2.2.4 - MessageBox

控件程序集还定义了一个 MessageBoxDlg 类和模板,与 WinForms 中现有的类和模板相匹配。它继承自 HeaderedDialog

模板非常简单,如下所示。按钮和图像将在运行时配置。

模板如下所示。

<ucc:HeaderedDialogWindow x:Class="ReflectionStudio.Controls.MessageBoxDlg"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:ucc="clr-namespace:ReflectionStudio.Controls"
    Height="240" Width="600">
<Grid Margin="10,-20,10,10">
    <Grid.RowDefinitions>
        <RowDefinition />
        <RowDefinition Height="32" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="0.33*" />
        <ColumnDefinition Width="0.33*" />
        <ColumnDefinition Width="0.33*" />
    </Grid.ColumnDefinitions>
    <TextBlock Margin="10" Grid.ColumnSpan="3"
        x:Name="textBlockMessage" HorizontalAlignment="Stretch"
        TextWrapping="Wrap"
        FontSize="18" Text="tes de message pour voir la taille"/>
    <Button Grid.Row="1" x:Name="BtnLeft"
        IsDefault="True" Margin="26,0,29,0"
        Click="Btn_Click" />
    <Button IsDefault="True" Margin="30,0,31,0"
        Name="BtnMidle" Grid.Column="1"
        Grid.Row="1" Click="Btn_Click"></Button>
    <Button IsDefault="True" Margin="28,0,33,0"
        Name="BtnRight" Grid.Column="2"
        Grid.Row="1" Click="Btn_Click"></Button>
</Grid>
</ucc:HeaderedDialogWindow>

MessageBoxDlg 有两个用于调用的 static 方法。

public static MessageBoxResult Show(string message, string title)
{
    return MessageBoxDlg.Show(message, title,
           MessageBoxButton.OKCancel, MessageBoxImage.None);
}

public static MessageBoxResult Show(string message,
       string title, MessageBoxButton button, MessageBoxImage icon)
{
    MessageBoxDlg msgBox = new MessageBoxDlg();

    msgBox.Title = title;
    msgBox.textBlockMessage.Text = message;
    msgBox.DisplayButton(button);
    msgBox.DisplayIcon(icon);
    msgBox.ShowDialog();

    return msgBox.MessageBoxResult;
}

使用时,这里是一个使用资源作为标题和消息的示例。

MessageBoxResult answer = MessageBoxDlg.Show(
        ReflectionStudio.Properties.Resources.MSG_PRJ_ASK_SAVE,
        ReflectionStudio.Properties.Resources.MSG_TITLE,
        MessageBoxButton.YesNoCancel,
        MessageBoxImage.Error);

2.3 - 控件

此命名空间包含 Reflection Studio 基本需求所需的所有通用控件。即将推出的控件是 PropertyGridWaitControlDiagram

2.3.1 - 按钮、标题等

  • StandaloneHeader 提供 TitleDescriptionImage 属性,并可在面板中使用,就像 DialogHeaderDialogWindow 中使用一样。
  • FlatImageButton 用于资源管理器顶部。它只显示图像,并具有鼠标悬停效果。
  • ImageButton 是带有图像的标准按钮。它具有附加的 ImagePositionOrientation 属性。

2.3.2 - 图片

AutoGreyableImage 是一个非常有用的图像控件,当父容器中的 IsEnabled 属性发生变化时,它会显示图像的灰色版本。以下是在上下文菜单项中使用它的示例。

<MenuItem Header="Delete"
    DataContext="{Binding Path=PlacementTarget,
         RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"
    Command="{x:Static local:DatabaseExplorer.DataSourceRefresh}"
    CommandParameter="{Binding Tag}"
    IsEnabled="{Binding Tag, Converter={StaticResource ObjectToBooleanConverter}}">
    <MenuItem.Icon>
        <controls:AutoGreyableImage
           Source="/ReflectionStudio;component/Resources/Images/
                   16x16/application/delete.png" Width="16"/>
    </MenuItem.Icon>
</MenuItem>

2.3.3 - Treeview

TreeViewExtended 类具有两个主要功能:

  1. 在右键单击发生之前,选择一个项,然后显示上下文菜单。
  2. 虚拟节点和 OnPopulateEvent - 这与树绑定不兼容,仅与手动填充兼容。
  3. 计划进行“新项”管理等。

我们通过以下代码管理树上的 PreviewMouseRightButtonDown 事件,以选择鼠标下方的树视图项。

/// <summary>
/// Allow to select an item before the context menu pop's up
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void TreeViewExtended_PreviewMouseRightButtonDown(object sender,
             MouseButtonEventArgs e)
{
    TreeView control = sender as TreeView;

    IInputElement clickedItem = control.InputHitTest(e.GetPosition(control));

    while ((clickedItem != null) && !(clickedItem is TreeViewItem))
    {
        FrameworkElement frameworkkItem = (FrameworkElement)clickedItem;
        clickedItem = (IInputElement)(frameworkkItem.Parent ??
                         frameworkkItem.TemplatedParent);
    }

    if (clickedItem != null)
        ((TreeViewItem)clickedItem).IsSelected = true;
}

树视图有一个 PopulateOnDemand 依赖属性和一个用于添加项的特殊函数。

/// <summary>
/// Add a new treeview item to the specified parent.
/// Manage dummy node and PopulateOnDemand
/// </summary>
public TreeViewItem AddItem(TreeViewItem parent,
       string label, object tag, bool needDummy = true)
{
    TreeViewItem node = new TreeViewItem();
    node.Header = label;
    node.Tag = tag;

    if (PopulateOnDemand && needDummy)
    {
        node.Expanded += new RoutedEventHandler(node_Expanded);
        node.Items.Add(new TreeViewItemDummy());
    }

    if (parent != null)
        parent.Items.Add(node);
    else
        this.Items.Add(node);

    return node;
}

此函数(如果您将 PopulateOnDemand 设置为 true - needDummy 参数默认为 true)将始终添加一个 TreeViewItemDummy 元素,以及一个事件处理程序来管理每个项的展开事件(在 WPF 的树控件中已不存在)。

当一个项展开时,树视图会将项类型与 TreeViewItemDummy 类型进行比较,如果需要,会移除它并发送一个 ItemNeedPopulateEvent

/// <summary>
/// Internal management of the treeview item expansion.
/// remove the dummy node if needed and fire the ItemNeedPopulateEvent
/// event when the PopulateOnDemand property is set
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void node_Expanded(object sender, RoutedEventArgs e)
{
    TreeViewItem opened = (TreeViewItem)sender;

    if (opened.Items[0] is TreeViewItemDummy && PopulateOnDemand)
    {
        opened.Items.Clear();
        RaiseItemNeedPopulate(opened);
    }
}

要使用它,首先在 OnItemNeedPopulate 事件处理程序上连接您的处理程序。

this.treeViewDB.OnItemNeedPopulate +=
   new TreeViewExtended.ItemNeedPopulateEventHandler(treeViewDB_OnItemNeedPopulate);

然后添加您的树项。

//server connection sample
TreeViewItem tn = this.treeViewDB.AddItem(null, source.Name, source);

如果您知道没有子项,请将 needDummy 参数设置为 false - AddItem 函数会处理它。

...
    foreach (IndexSchema ts in ((TableSchema)parent.Tag).Indexes)
        this.treeViewDB.AddItem(openedItem, ts.Name, ts, false);
...

2.3.4 - 帮助程序

控件程序集中有各种帮助程序。

  • LongOperation:管理光标并启动/停止进度条。
  • using (new LongOperation(this, "Execute"))
    {
        //...very long operation or not threaded so ui must not respond...
    }
  • VisualHelper:允许将视觉元素保存为 BitmapImage

2.4 - 转换器

基本转换器在 Controls 程序集中。您将在主程序中找到其他转换器。

  • ReflectionStudio.Controls
    • BoolToVisibilityConverter
    • EnumToStringConverter:用于简单的 string 转换,当 enum 是人类可读的时候。
  • ReflectionStudio
    • ScaleToPercentConverter:双精度值到百分比及其反向。
    • ObjectToBooleanConverter:将对象(null 或非 null)转换为布尔值,用于启用菜单项。
    • NetTypeToImageConverter:用于程序集树视图显示类型图像。
    • LogTypeToImageConverter:用于日志工具箱显示错误图标。
    • FileInfoToImageConverter:用于模板树视图显示文件夹或文件图像。
    • DockStateToBooleanConverter:将 Avalon DockState enum 转换为可见性布尔值。
    • DBTypeToImageConverter:用于数据库树视图显示对象图像。

2.5 - 外部库

2.5.1 - AvalonDock 和 Fluent

集成 CodePlex 的 AvalonDockFluent 库从 MainWindow 开始。使用以下三行定义窗口的主骨架:RibbonContentStatusBar

<Fluent:RibbonWindow x:Class="ReflectionStudio.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Fluent="clr-namespace:Fluent;assembly=Fluent"
    xmlns:ad="clr-namespace:AvalonDock;assembly=AvalonDock"
    xmlns:cmd="clr-namespace:ReflectionStudio.Classes"
    xmlns:Controls=
      "clr-namespace:ReflectionStudio.Controls;
       assembly=ReflectionStudio.Controls"
    xmlns:UserControls="clr-namespace:ReflectionStudio.Components.UserControls"
    xmlns:converters="clr-namespace:ReflectionStudio.Components.Converters"
    ResizeMode="CanResizeWithGrip"
    Title="{Binding Title}" Height="600" Width="800"
    Loaded="OfficeWindow_Loaded"
    Closing="OfficeWindow_Closing" Drop="OfficeWindow_Drop"
    Icon="Resources\Images\16x16\ReflectionStudio.png">

<Fluent:RibbonWindow.Resources...

    <Grid Name="MainGrid">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <!--RIBBON CONTROL-->
        <Fluent:Ribbon ...

        <!--CONTENT-->
        <ad:DockingManager ...

        <!--STATUS BAR-->
        <UserControls:StatusBar Grid.Row="2" x:Name="MainStatusBar" />

    </Grid>

</Fluent:RibbonWindow>

非常简单,我对 Fluent 控件的实现基于项目文档。更棘手的是 AvalonDock 内容的布局。一开始我遇到了一些调整大小的问题,并通过以下布局解决了:一个主垂直 ResizingPanel 包含两个水平 ResizingPanel 和底部的日志资源管理器。

layout

幸运的是,资源管理器是 UserControls,因此主 XAML 保持可读。以下是内容结构。

<ad:DockingManager Grid.Row="1" x:Name="_dockingManager"
         Loaded="_dockingManager_Loaded"
         Background="{DynamicResource DefaultBorderBrush}">
    <ad:ResizingPanel Orientation="Vertical">
        <ad:ResizingPanel Orientation="Horizontal">
            <ad:ResizingPanel Orientation="Vertical"
                     ad:ResizingPanel.ResizeWidth="200">
                <!--LEFT PART-->
                <ad:DockablePane>

                    <!--ASSEMBLY EXPLORER -->
                    <UserControls:AssemblyExplorer x:Name="_DllExplorerDock" />

                    <!--DATABASE EXPLORER -->
                    <UserControls:DatabaseExplorer x:Name="_DBExplorerDock" />

                    <!--DATABASE EXPLORER -->
                    <UserControls:TemplateExplorer x:Name="_TemplateExplorerDock" />

                </ad:DockablePane>

            </ad:ResizingPanel>

            <!--CENTER PART-->
            <ad:DocumentPane x:Name="_documentsHost">

                <!--here goes the documents-->

            </ad:DocumentPane>

            <ad:ResizingPanel Orientation="Vertical"
                       ad:ResizingPanel.ResizeWidth="200">
                <!--RIGHT PART-->
                <ad:DockablePane>

                    <!--PROJECT EXPLORER-->
                    <UserControls:ProjectExplorer x:Name="_ProjectExplorerDock" />

                    <!--PROPERTY EXPLORER-->
                    <UserControls:PropertyExplorer x:Name="_PropertyExplorerDock" />

                </ad:DockablePane>

            </ad:ResizingPanel>
        </ad:ResizingPanel>

    <ad:ResizingPanel Orientation="Horizontal">

        <!--LOGS EXPLORER-->
        <ad:DockablePane>
            <UserControls:EventLogExplorer x:Name="_LogExplorerDock" />
        </ad:DockablePane>
    </ad:ResizingPanel>

</ad:ResizingPanel>
</ad:DockingManager>

2.6 - 通用控件

在本章中,我将描述应用程序中不适合放在任何其他文章部分的类和用户控件。我们有将在稍后讨论的资源管理器、StatusBar 和文档。

explorers

2.6.1 - 文档和 StatusBar

Reflection Studio UI 中的所有内容都基于 Document 和 Explorers。每个资源管理器都派生自 DockableContent,并且很简单。文档派生自 ZoomDocument 或直接派生自 AvalonDock 中的 DocumentContent

这是最有趣的部分:ZoomDocument 持有一个 Scale 属性和一个 ScaleTransformer,您必须在派生类中将其与内容关联。

//QueryDocument
SyntaxEditor.TextArea.LayoutTransform = base.ScaleTransformer;

由于它在预览模式下管理鼠标滚轮事件,我们可以更新缩放比例、转换器,并引发缩放事件。

/// <summary>
/// Handle CTRL WHEEL for zooming
/// </summary>
/// <param name="e"></param>
protected override void OnPreviewMouseWheel(System.Windows.Input.MouseWheelEventArgs e)
{
    base.OnPreviewMouseWheel(e);

    //zooming
    if (Keyboard.IsKeyDown(Key.LeftCtrl))
    {
        UpdateContent(e.Delta > 0);
        e.Handled = true;
    }
}

主部分显示在下面。通过连接 DockingManagerPropertyChanged 事件来捕获活动文档,以获取活动文档,这样我们就可以处理两个方向的缩放已更改事件。如果需要(对于基于 ZoomDocument 的类),我会移除前一个活动文档的缩放处理程序,然后将其添加到新文档。请注意,StatusBar 控件有一个 CanZoom 属性来禁用缩放滑块,并且文档最初会设置缩放值。

void DockingManagerPropertyChanged(object sender, PropertyChangedEventArgs e)
{
    //manage the active document
    if (e.PropertyName == "ActiveDocument" && _dockingManager.ActiveDocument != null)
    {
        Tracer.Verbose("MainWindow.DockingManagerPropertyChanged",
                       "[{0}] '{1}' is the active document",
                       DateTime.Now.ToLongTimeString(),
                       _dockingManager.ActiveDocument.Title);

        //remove the previous doc from event handling
        if (ActiveDocument is ZoomDocument)
        {
            this.MainStatusBar.ZoomChanged -=
              new EventHandler<ZoomRoutedEventArgs>(
              ((ZoomDocument)ActiveDocument).OnZoomChanged);
            ((ZoomDocument)ActiveDocument).ZoomChanged -=
              new ZoomDocument.ZoomChangedEventHandler(
              this.MainStatusBar.OnZoomChanged);
        }

        // save the new active doc
        ActiveDocument = (DocumentContent)_dockingManager.ActiveDocument;

        //plug to zoom event handling if needed
        if (this._dockingManager.ActiveDocument is ZoomDocument)
        {
            ZoomDocument zd =
              (ZoomDocument)this._dockingManager.ActiveDocument;

            this.MainStatusBar.CanZoom = true;
            this.MainStatusBar.sliderZoom.Value = zd.Scale;

            //root status bar update to the document
            this.MainStatusBar.ZoomChanged +=
              new EventHandler<ZoomRoutedEventArgs>(zd.OnZoomChanged);

            //root doc event to the status bar
            ((ZoomDocument)this._dockingManager.ActiveDocument).ZoomChanged +=
              new ZoomDocument.ZoomChangedEventHandler(
              this.MainStatusBar.OnZoomChanged);
        }
        else
        {
            //disable the zoom slider
            this.MainStatusBar.CanZoom = false;
        }
....

2.6.2 - 通用主页和帮助文档

HelpDocument 只是被模板化以包含一个 XPS 查看器,我们使用以下代码在加载函数中设置 DocumentViewer 属性。

XpsDocument xpsHelp = new XpsDocument(System.IO.Path.Combine(
      PathHelper.ApplicationPath, ((DocumentDataContext)DataContext).FullName),
      System.IO.FileAccess.Read);
documentViewer1.Document = xpsHelp.GetFixedDocumentSequence();

有关截图,请参阅 第一部分。请注意,要显示的文档是通过命令作为参数传入的,并在 MainWindow.xaml 中定义。

HomeDocument 稍微复杂一些。由于我希望它能从互联网上更新,我必须包含一个默认的“供稿不可用内容”,然后添加一个默认情况下可保存/加载的资源,以及一个更新函数以从互联网获取最新版本。启动时,我创建一个 BackgroundWorker 来获取 URL 内容,因此文档会显示“供稿不可用内容”。

/// <summary>
/// Load the XAML if not in design mode in a background thread
/// </summary>
public override void OnApplyTemplate()
{
    Tracer.Verbose("HomeDocument:OnApplyTemplate", "START");

    if (!DesignerProperties.GetIsInDesignMode(this))
    {
        try
        {
            string urlToRead = "http://i3.codeplex.com/Project/Download/" +
                   "FileDownload.aspx?ProjectName=ReflectionStudio&DownloadId=132959";
            string destFile = System.IO.Path.Combine(
                   PathHelper.ApplicationPath, "Home.xaml");

            BackgroundWorker webWorker = new BackgroundWorker();
            webWorker.WorkerReportsProgress = false;
            webWorker.WorkerSupportsCancellation = false;
            webWorker.DoWork += new DoWorkEventHandler(bw_DoWork);
            webWorker.RunWorkerCompleted +=
               new RunWorkerCompletedEventHandler(bw_RunWorkerCompleted);

            // Start the asynchronous operation.
            webWorker.RunWorkerAsync(new UrlSaveHelper(urlToRead, destFile));

        }
        catch (Exception all)
        {
            Tracer.Error("HomeDocument.OnApplyTemplate", all);
        }
    }

    Tracer.Verbose("HomeDocument:OnApplyTemplate", "END");
}

之后,如果工作成功,我们尝试加载 XAML(在代理响应的情况下是错误的)——这就是为什么我在 catch 语句中保存和加载资源文件。最后,我解析 XAML 以将 HyperLink 元素与代码隐藏关联起来。

/// <summary>
/// After the thread complete, load the xaml home document
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    // First, handle the case where an exception was thrown.
    if (e.Error != null)
    {
        ...
    }
    else
    {
        Tracer.Verbose("HomeDocument:bw_RunWorkerCompleted", "START");

        UrlSaveHelper hlp = (UrlSaveHelper)e.Result;

        string destFile =
          System.IO.Path.Combine(PathHelper.ApplicationPath, "Home.xaml");

        // Finally, handle the case where the operation succeeded.
        if (File.Exists(destFile))
        {
            try
            {
                FileStream fs = File.OpenRead(destFile);
                FlowDocViewer.Document = (FlowDocument)XamlReader.Load(fs);
                fs.Close();

                Tracer.Verbose("HomeDocument:bw_RunWorkerCompleted",
                               "Internet document loaded");
            }
            catch (Exception)
            {
                //load the config from resources
                using (Stream fs = 
		Application.ResourceAssembly.GetManifestResourceStream(
                             "ReflectionStudio.Resources.Embedded.Home.xaml"))
                {
                    if (fs == null)
                        throw new InvalidOperationException(
                              "Could not find embedded resource");

                    FlowDocViewer.Document = (FlowDocument)XamlReader.Load(fs);
                    fs.Close();

                    Tracer.Verbose("HomeDocument:bw_RunWorkerCompleted",
                                   "Local document loaded");
                }
            }

            ParseHyperlink(FlowDocViewer.Document);

            Tracer.Verbose("HomeDocument:bw_RunWorkerCompleted", "END");
        }
    }
}

计划进行一项改进,以开发一个 RSS 提要控件来获取 CodePlex 或 CodeProject 的新闻,并将其嵌入其中。

2.6.3 - Document Factory

对于所有这些文档用户控件类型,我创建了一个 DocumentFactory 来帮助进行文档管理。这个类是一个单例,它连接到 AvalonDockManager 以及所有遵循以下规则的文档。

  1. 基于 Avalon 的 DocumentContent
  2. 支持打开、关闭、保存等命令,并在最近文件列表中提供更新。
  3. 支持预览图像(?)
  4. 支持基于其类型和关联文件的通用数据上下文。

该工厂有一些简单的函数,如 FindOpenGet……下面是如何打开前面讨论的帮助和主页文档的示例。我不会进一步描述这个模块,因为它将会改变。

private void DisplayHomeDocument()
{
	DocumentFactory.Instance.OpenDocument
		(DocumentFactory.Instance.SupportedDocuments.Find
		(p => p.DocumentContentType == typeof(HomeDocument)),
		new DocumentDataContext() { FullName = "Home", Entity = null });
}

private void DisplayHelpDocument(string fileName)
{
	DocumentFactory.Instance.OpenDocument
		(DocumentFactory.Instance.SupportedDocuments.Find
		(p => p.DocumentContentType == typeof(HelpDocument)),
		new DocumentDataContext() { FullName = fileName, Entity = null });
}

或者像下面的通用“新建文档”函数,我们通过传递类型或文件扩展名来创建一个文档。

public void NewCommandHandler(object sender, ExecutedRoutedEventArgs e)
{
	if (string.IsNullOrEmpty((string)e.Parameter)) //default
	{
		//display the dialog
		NewDocumentDlg Dlg = new NewDocumentDlg();
		Dlg.Owner = Application.Current.MainWindow;
		Dlg.DataContext = DocumentFactory.Instance.SupportedDocuments.Where
			( p => p.CanCreate == true ).ToList();

		if (Dlg.ShowDialog() == true)
			DocumentFactory.Instance.CreateDocument
				( Dlg.DocumentTypeSelected );
	}
	else //file type as parameter
	{
		DocumentFactory.Instance.CreateDocument
		(DocumentFactory.Instance.SupportedDocuments.Find
			(p => p.Extension == (string)e.Parameter));
	}
	e.Handled = true;
}

结论/反馈

下一篇文章再见。请随时在 Codeplex 或 CodeProject 上给我反馈。随着团队的不断壮大,我希望我们的速度越来越快,并且欢迎您的加入!

文章/软件历史

  1. 初始发布 - 版本 BETA 0.2
    • 包含“几乎”所有内容的初始版本,可在 第一部分CodePlex 上作为 Release 下载。
  2. 版本 BETA 0.3
    • 更新了皮肤/颜色管理和数据库模块。
    • 第四部分 - 数据库模块 - 已发布。
© . All rights reserved.