一个简单、集成的 WPF 帮助系统






4.97/5 (15投票s)
让您的用户在不离开应用程序的情况下获得快速、直观的帮助。
引言
我最近遇到了为我创建的 WPF 应用程序创建一个帮助系统这个常见问题。该程序是一个“标准的”业务应用程序(按钮、列表等),所以我认为可能有一些内置的支持系统或新的标准可以轻松地为用户提供帮助。令我惊讶的是,没有明确的标准,所以我决定自己创建一个。
背景
起初,我以为 WPF 会内置支持像 CHM 文件这样的帮助系统标准。经过一些调查,我发现了 AutomationProperties.HelpText
附加属性,但发现共识似乎是它是一个不完整的特性,或者被添加用于将来使用。我在框架中找不到任何实际直接使用 HelpText
属性的东西,而且目前,它显然是为了被客户端应用程序使用,而不是驱动一个自动化系统(我太傻了,我把“自动化”这个词理解得太 literal 了)。
潜在的解决方案
在创建我的解决方案之前,我想到了为用户提供帮助系统的一些现有方法(出于业务原因,第三方和/或开源解决方案几乎不是选项)。这个部分列表解决了许多我遇到的问题。
- 外部文档
- 优点
- 嗯……给我一分钟……
- 缺点
- 一旦你把东西写进文档,它就过时了。
- 用户必须找到并打开一个外部文件,然后在应用程序和文档之间切换。
- 我不想每天花时间把截图粘贴到 Word 里。
- 工具提示
- 优点
- 用户熟悉。
- 内置于 WPF。
- 缺点
- 无法判断控件是否有工具提示;用户必须悬停并等待每个控件。
- 工具提示通常只包含几个词,而我可能需要两三句话。
- “始终开启” - 一旦你知道一个控件的作用,你很可能就不会再需要这个工具提示了。然而,如果你将鼠标悬停在控件上,它就会弹出,无论如何。如果工具提示中有大量文本,这可能会很烦人。
- 侧边栏帮助(类似 MS Office)
- 优点
- 用户熟悉。
- 可以关闭/隐藏。
- 用于处理大量文本。
- 缺点
- 最适合强大的、完全索引和链接的帮助系统。
- 更改现有 UI 布局。
在查看了这些选项(以及其他选项)之后,我决定我最喜欢工具提示的工作方式。它们告诉用户在他们操作他们指向的控件时会发生什么。然而,问题是我想能够像侧边栏帮助那样关闭它们。
我的解决方案
我决定任何形式的外部解决方案都不在考虑之列。WPF 具有许多高级功能,我没有理由强迫用户离开我的应用程序,甚至打开一个新窗口。我也不希望帮助“碍事”,所以我知道它必须能够轻松地打开和关闭。我提出的就是这个简单但功能强大的解决方案。
按下 F1 键会在有帮助的控件周围显示一个黄色高亮。当鼠标悬停在控件上时,帮助文本会以工具提示的方式显示(没有延迟),使用方便的 Popup
类。当用户完成时,再次按下 F1 会移除高亮,帮助就不再显示。在上面的示例中,如果鼠标悬停在按钮“Two”上,则显示组合框的帮助,因为“Two”没有帮助。当你将鼠标移动到按钮“Three”上时,会显示该按钮的帮助。当然,当鼠标悬停在画布或没有帮助的控件上时,帮助气泡会消失。
工作原理
实现此功能所需代码相当简单。不过,让我先说一下,这个解决方案并未优化,我确信我们中间的 WPF 信徒可能会想出更好的方法来遍历视觉树或使用更有效的视觉效果(BitmapEffect
s 并非如此)。我将很乐意听取建议。
关键类/属性
System.Windows.Automation.AutomationProperties.HelpText
System.Windows.Controls.Primitives.Popup
System.Windows.Media.VisualTreeHelper
System.Windows.Media.Effects.OuterGlowBitmapEffect
步骤 1 - XAML
<Window ...usual stuff... Name="winMain" KeyDown="winMain_KeyDown">
<Canvas Name="canvMain">
<Button Content="No Help" ... />
<Button Content="Has Help" AutomationProperties.HelpText="I have help" ... />
...just the basic controls...
</Window>
步骤 2 - 成员 / 扩展方法
public static class StringUtils
{
public static bool IsNothing(this string value)
{
return value == null || value.Trim().Length == 0;
}
}
// Members in Window1
private DependencyObject CurrentHelpDO { get; set; }
private Popup CurrentHelpPopup { get; set; }
private bool HelpActive { get; set; }
private MouseEventHandler _helpHandler = null;
private readonly OuterGlowBitmapEffect YellowGlow =
new OuterGlowBitmapEffect()
{ GlowColor = Colors.Yellow, GlowSize = 10, Noise = 1 };
步骤 3 - F1 键导致帮助切换
private void winMain_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.F1)
{
e.Handled = true;
ToggleHelp();
}
}
步骤 4 - 递归地通过视觉树切换帮助
private void ToggleHelp()
{
// Turn the current help off
CurrentHelpDO = null;
if (CurrentHelpPopup != null)
{
CurrentHelpPopup.IsOpen = false;
}
// Toggle current state; add/remove mouse handler
HelpActive = !HelpActive;
if (_helpHandler == null)
{
_helpHandler = new MouseEventHandler(winMain_MouseMove);
}
if (HelpActive)
{
winMain.MouseMove += _helpHandler;
}
else
{
winMain.MouseMove -= _helpHandler;
}
// Start recursive toggle at visual root
ToggleHelp(canvMain);
}
private void ToggleHelp(DependencyObject dependObj)
{
// Continue recursive toggle. Using the VisualTreeHelper works nicely.
for (int x = 0; x < VisualTreeHelper.GetChildrenCount(dependObj); x++)
{
DependencyObject child = VisualTreeHelper.GetChild(dependObj, x);
ToggleHelp(child);
}
// BitmapEffect is defined on UIElement so our DependencyObject
// must be a UIElement also
if (dependObj is UIElement)
{
UIElement element = (UIElement)dependObj;
if (HelpActive)
{
string helpText = AutomationProperties.GetHelpText(element);
if (!helpText.IsNothing())
{
// Any effect can be used, I chose a simple yellow highlight
((UIElement)element).BitmapEffect = YellowGlow;
}
}
else if (element.BitmapEffect == YellowGlow)
{
element.BitmapEffect = null;
}
}
}
步骤 5 - 跟踪鼠标
private void winMain_MouseMove(object sender, MouseEventArgs e)
{
// You can check the HelpActive property if desired, however
// the listener should not be hooked up so this should not be firing
HitTestResult hitTestResult =
VisualTreeHelper.HitTest(((Visual)sender), e.GetPosition(this));
if (hitTestResult.VisualHit != null &&
CurrentHelpDO != hitTestResult.VisualHit)
{
// Walk up the tree in case a parent element has help defined
DependencyObject checkHelpDO = hitTestResult.VisualHit;
string helpText = AutomationProperties.GetHelpText(checkHelpDO);
while (helpText.IsNothing() && checkHelpDO != null
&& checkHelpDO != canvMain && checkHelpDO != winMain)
{
checkHelpDO = VisualTreeHelper.GetParent(checkHelpDO);
helpText = AutomationProperties.GetHelpText(checkHelpDO);
}
if (helpText.IsNothing() && CurrentHelpPopup != null)
{
CurrentHelpPopup.IsOpen = false;
CurrentHelpDO = null;
}
else if (!helpText.IsNothing() && CurrentHelpDO != checkHelpDO)
{
CurrentHelpDO = checkHelpDO;
// New visual "stack" hit, close old popup, if any
if (CurrentHelpPopup != null)
{
CurrentHelpPopup.IsOpen = false;
}
// Obviously you can make the popup look anyway you want with
// any number of options. I chose a simple tooltip look-and-feel.
// (caching/reuse omitted for example)
CurrentHelpPopup = new Popup()
{
AllowsTransparency = true,
PopupAnimation = PopupAnimation.Scroll,
PlacementTarget = (UIElement)hitTestResult.VisualHit,
Child = new Border()
{
CornerRadius = new CornerRadius(10),
BorderBrush = new SolidColorBrush(Colors.Goldenrod),
BorderThickness = new Thickness(2),
Background = new SolidColorBrush(Colors.LightYellow),
Child = new TextBlock()
{
Margin = new Thickness(10),
Text = helpText.Replace("\\r\\n", "\r\n"),
FontSize = 14,
FontWeight = FontWeights.Normal
}
}
};
CurrentHelpPopup.IsOpen = true;
}
}
}
更新 - 2010/2/28
这个解决方案中我不太喜欢的一点是,遍历视觉树似乎不太符合 WPF 的风格。在 Pete O'Hanlon 提醒我注意内置的 ApplicationCommand.Help
命令后,我再次审视了一下,并提出了另一种方法。这很简单,你只需要创建一个值转换器,将布尔 HelpActive
属性(现在是一个 DependencyProperty
)转换为 BitmapEffect
。如果帮助是激活的,你就返回黄色的发光对象;否则就返回 null
。唯一的问题是,你必须绑定每个设置了帮助的控件的 BitmapEffect
属性。这可以单独进行,也可以按 Style
进行。如果有人知道一种方法可以将 BitmapEffect
和 HelpText
属性关联起来,让你只需要设置帮助,请告诉我。
[ValueConversion(typeof(bool), typeof(BitmapEffect))]
public class GlowConverter : IValueConverter
{
private OuterGlowBitmapEffect _glow = null;
public GlowConverter()
{
_glow = new OuterGlowBitmapEffect()
{ GlowSize = 10, Noise=1, GlowColor = Colors.Yellow };
}
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
return ((bool)value) ? _glow : null;
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
return value != null;
}
}
-----------
public partial class Window1 : Window
{
public static readonly DependencyProperty HelpActiveProperty =
DependencyProperty.Register("HelpActive", typeof(bool), typeof(Window1),
new FrameworkPropertyMetadata(false,
FrameworkPropertyMetadataOptions.AffectsRender));
public bool HelpActive
{
get
{
return (bool)GetValue(HelpActiveProperty);
}
set
{
SetValue(HelpActiveProperty, value);
}
}
public Window1()
{
InitializeComponent();
CommandBindings.Add(new CommandBinding(ApplicationCommands.Help,
(x, y) => HelpActive = !HelpActive,
(x, y) => y.CanExecute = true));
}
}
App.xaml
<gui:GlowConverter x:Key="GlowChange" />
<Style TargetType="Button">
<Setter Property="Background" Value="Red" />
<Setter Property="BitmapEffect"
Value="{Binding Source={x:Static win:Application.Current},
Path=MainWindow.HelpActive, Converter={StaticResource GlowChange}}" />
</Style>
结论
尽管 WPF 没有内置的帮助系统,但令我惊喜的是,它创建起来如此容易。我首先承认这个解决方案并不完美,但我认为它非常适合许多不同的应用程序。
更新历史
- 2010/2/28 - 添加了备用方法。
- 2010/2/17 - 初始版本。