如何在 Silverlight 2 中实现自定义光标






4.78/5 (8投票s)
本文介绍了如何在 Silverlight 中实现自定义光标。
引言
Silverlight 2 只支持一小部分默认光标类型。WPF (Windows Presentation Foundation) 为开发人员提供了扩展的默认光标类型集,此外,为 WPF 应用程序创建自定义光标也是一项简单的任务。
然而,在 Silverlight 2 中模拟自定义光标却相当简单。有不同的实现方法来显示光标。本文介绍的解决方案基于 Popup
类和一组预先定义好的自定义光标图像,这使得使用起来很方便。让我们先来看一张截图,展示一个绿色矩形上带有自定义光标类型(十字准星)。
自定义光标实现与一个 UI 元素堆栈协同工作,所有元素都使用不同的光标,并且在滚动查看器内。上面显示的页面的 XAML 代码如下:
...
<Grid Background="White">
<ScrollViewer Width="400" Height="400" ... >
<Grid>
<Rectangle parago:CursorSet.ID="SizeAll" Fill="LightCoral" Width="600" ... />
<Rectangle parago:CursorSet.ID="Cross" Fill="Red" Width="150" Height="150" ... />
<Rectangle parago:CursorSet.ID="Hand" Fill="Blue" Width="130" Height="130" ... />
<Rectangle parago:CursorSet.ID="IBeam" Fill="Yellow" Width="100" Height="100" ... />
<Rectangle parago:CursorSet.ID="Cross" Fill="Green" Width="50" Height="50" ... />
</Grid>
</ScrollViewer>
</Grid>
...
代码显示,设置自定义光标就像在 Silverlight 2 中设置默认和内置光标一样简单。像 Hand 或 IBeam 这样的标准光标类型也可以定义为 ID,并会在内部转换为设置 Cursor
属性。因此,从开发者的角度来看,设置 UI 元素光标只有一种技术可以使用,无需在 XAML 代码中添加任何额外的元素。
这是如何实现的?有一个名为 CursorSet
的类,它实现了一个名为 ID
的附加属性。在此实现中使用附加属性可以查询 UI 元素的光标设置。CursorSet
类维护一个静态光标 ID 列表、一个名为 ActiveElements
的活动元素列表(带有附加的光标 ID),以及一个用于显示实际光标图像的静态 popup 和 canvas 实例。
public class CursorSet
{
const string ImagePath = "/Resources/Cursors/";
internal static Popup Popup;
internal static Canvas AdornerLayer;
internal static List<string> IDs;
internal static Dictionary<FrameworkElement,
ContentControl> ActiveElements;
#region public string ID (attached)
public static string GetID(DependencyObject d)
{
return (string)d.GetValue(IDProperty);
}
public static void SetID(DependencyObject d, string id)
{
d.SetValue(IDProperty, id);
}
public static readonly DependencyProperty IDProperty =
DependencyProperty.RegisterAttached(
"ID",
typeof(string),
typeof(CursorSet),
new PropertyMetadata(new PropertyChangedCallback(OnIDPropertyChanged)));
static void OnIDPropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
FrameworkElement element = d as FrameworkElement;
string id = e.NewValue as string;
if(IsValidID(id))
{
Cursor cursor;
if(IsSystemType(id, out cursor))
{
element.Cursor = cursor;
RemoveMouseEventHandlers(element);
if(ActiveElements.ContainsKey(element))
ActiveElements.Remove(element);
}
else
{
AddMouseEventHandlers(element);
ContentControl control = CreateControl(id);
if(ActiveElements.ContainsKey(element))
ActiveElements[element] = control;
else
ActiveElements.Add(element, control);
}
}
else
{
element.Cursor = null;
RemoveMouseEventHandlers(element);
if(ActiveElements.ContainsKey(element))
ActiveElements.Remove(element);
}
}
#endregion
...
}
有关 Silverlight 中附加属性的更多信息,请参阅 MSDN 文档,或 SilverlightShow.net 网站上的这篇精彩文章。
附加属性 ID 的更改处理起着重要作用。当一个光标 ID 被新分配给一个 UI 元素(parago:CursorSet.ID="Cross"
)或者它已更改时,会调用 OnIDPorpertyChanged
方法。该方法首先检查定义的 ID 是否有效,然后检查 ID 是否是默认光标类型的名称。如果 ID 被识别为默认光标,则该方法将设置 UI 元素的 Cursor
属性,并移除所有现有的鼠标事件处理程序注册;否则,将为该元素创建一个 ContentControl
类的新实例,并将所需的自定义光标 ID 作为图像。然后将该实例添加到 ActiveElements
列表中,CursorSet
类会注册该 UI 元素的 MouseEnter
、MouseMove
和 MouseLeave
事件。
对于每个自定义 ID,都必须有一个 PNG 图像存在于字符串常量 ImagePath 定义的文件夹中。目前样本项目中只有两个光标可用,但很容易扩展自定义光标的集合。
由于我们需要将光标渲染在所有其他元素之上,并且独立于这些元素的布局,因此使用内置的 Popup
类非常完美。CusorSet
popup 本身包含一个 canvas 元素作为子元素,用于通过坐标设置光标图像的位置。Popup 的大小应完全填充 Silverlight 内容区域。因此,CursorSet
将 OnContentResized
方法注册到 Application.Current.Host.Content.Resized
事件,并相应地更新 popup 及其 canvas 的大小。
...
static void OnContentResized(object sender, EventArgs e)
{
if(AdornerLayer != null)
{
AdornerLayer.Width = Application.Current.Host.Content.ActualWidth;
AdornerLayer.Height = Application.Current.Host.Content.ActualHeight;
}
}
static void EnsurePopup()
{
if(Popup == null || AdornerLayer == null)
{
AdornerLayer = new Canvas()
{
IsHitTestVisible = false,
Width = Application.Current.Host.Content.ActualWidth,
Height = Application.Current.Host.Content.ActualHeight
};
Popup = new Popup
{
IsHitTestVisible = false,
Child = AdornerLayer
};
}
}
...
上面的列表还显示了 EnsurePopup
方法,该方法最初创建 popup 和 canvas 实例。重要的是要注意,必须将 IsHitTestVisible
属性设置为 false
,以避免与底层元素发生冲突。
MouseEnter
、MouseMove
和 MouseLeave
事件的鼠标事件处理程序定义如下:
...
static void OnElementMouseEnter(object sender, MouseEventArgs e)
{
EnsurePopup();
FrameworkElement element = sender as FrameworkElement;
ContentControl control = ActiveElements[element];
element.Cursor = Cursors.None;
AdornerLayer.Children.Add(control);
Point p = e.GetPosition(null);
Canvas.SetTop(control, p.Y);
Canvas.SetLeft(control, p.X);
Popup.IsOpen = true;
}
static void OnElementMouseMove(object sender, MouseEventArgs e)
{
FrameworkElement element = sender as FrameworkElement;
ContentControl control = ActiveElements[element];
Point p = e.GetPosition(null);
Canvas.SetTop(control, p.Y);
Canvas.SetLeft(control, p.X);
}
static void OnElementMouseLeave(object sender, MouseEventArgs e)
{
FrameworkElement element = sender as FrameworkElement;
ContentControl control = ActiveElements[element];
element.Cursor = null;
AdornerLayer.Children.Remove(control);
Popup.IsOpen = false;
}
...
在 MouseEnter
事件处理程序 OnElementMouseEnter
中,首先,代码将通过调用 EnsurePopup
方法(见上面的代码)来确保 popup 实例存在并已设置。由于事件处理程序仅为自定义光标注册,而不是为默认和内置光标注册,因此该方法将从 ActiveElements
列表中获取 ContentControl
实例(在设置 ID 属性时生成),并将其添加到 popup 的名为 AdornerLayer
的 canvas 中。接下来,光标图像的位置将被调整为当前鼠标指针位置,并且 UI 元素的 Cursor
属性将被设置为 None
值,以避免出现多个光标。然后,popup 将被显示。
MouseMove
事件处理程序 OnElementMouseMove
将根据当前鼠标位置更新 popup canvas 中光标图像的位置。一旦鼠标指针离开 UI 元素,带有光标图像的 ContentControl
将从 popup canvas 中移除,并且 popup 本身将关闭。
就是这样。