使用响应扩展的响应面(Rx)

  • 下载demo - 196 KB
  • 下载source - 98 KB

表的内容 系统要求反应面一个简单的计时器从事件中收集数据序列使用更复杂的查询订阅您希望完成的面最终考虑历史 介绍 “Rx”代表反应性扩展,它是微软DevLabs的项目计划之一。DevLabs是微软团队开发具有代表性技术的地方。这样的原型项目被发布,然后由开发社区进行评估,根据它们的成功,有一天它们可能会成为。net框架的一部分,或者成为一个新的工具,等等。 从. net框架的第一个版本开始,甚至更早之前,开发人员就一直在处理各种各样的事件:UI事件(如按键和鼠标移动)、时间事件(如计时器滴答)、异步事件(如Web服务响应异步调用),等等。反应性扩展诞生于DevLabs团队设想这些事件类型之间的“共性”时。他们努力为我们提供以更聪明的方式处理不同事件的工具。本文展示了一些可用于响应式扩展的实用技术,希望它们对您将来的项目有用。 系统需求 要使用本文提供的WPF ReactiveFace,如果您已经拥有Visual Studio 2010,那么这就足以运行应用程序了。如果你没有,你可以下载以下100%免费的开发工具直接从微软: 此外,你必须下载和安装Rx for .NET Framework 4.0通过点击按钮相同的名字从DevLabs页面: Visual c# 2010 express devlabs: .NET (Rx)响应式扩展 反应面项目 反应面是一个利用反应扩展的小WPF项目。你在屏幕上看到的这个丑陋的、令人毛骨悚然的、不完整的小脑袋只是用来说明一些Rx特性的借口。 当你运行这个项目时,你会注意到的第一件事是眼皮在眨(即使没有眼球!)正如我之前所说的,这个小应用程序是用来演示Rx的特性的,所以让我们从blink开始。 眨眼本身是由一个移动“皮肤”的动画完成的,也就是覆盖在脸部眼洞后面的矩形。一旦动画开始,矩形快速地上下移动,模拟一个眨眼。 动画存储在故事板中,故事板又存储在窗口XAML中: 隐藏,收缩,复制Code

<Window.Resources>
    ...
        <Storyboardx:Key="sbBlinkLeftEye">
            <DoubleAnimationx:Name="daBlinkLeftEye"Storyboard.TargetName="recEyelidLeft"Storyboard.TargetProperty="Height"From="18"To="48"Duration="0:0:0.100"AutoReverse="True">
            </DoubleAnimation>
        </Storyboard>
        <Storyboardx:Key="sbBlinkRightEye">
            <DoubleAnimationx:Name="daBlinkRightEye"Storyboard.TargetName="recEyelidRight"Storyboard.TargetProperty="Height"From="18"To="48"Duration="0:0:0.100"AutoReverse="True">
            </DoubleAnimation>
        </Storyboard>
</Window.Resources>

一个简单的定时器 下面的代码将启动故事板,这样闪烁将每2000毫秒(2秒)发生一次。如果您对动画有一点了解,我相信您现在一定在问自己:“为什么不在XAML本身中设置一个重复行为为永远?”好吧,虽然您是对的,但我必须说这只是为了说明如何使用Rx代码来实现这一点。 隐藏,复制Code

//Find and store storyboard resources
var sbBlinkLeftEye = (Storyboard)FindResource("sbBlinkLeftEye");
var sbBlinkRightEye = (Storyboard)FindResource("sbBlinkRightEye");

//Set a new observable sequence which produces
//a value each 2000 milliseconds
var blinkTimer = Observable.ObserveOnDispatcher(
    Observable.Interval(TimeSpan.FromMilliseconds(2000))
    );

//Subscribe to the timer sequence, in order
//to begin both blinking eye storyboards
blinkTimer.Subscribe(e =>
    {
        sbBlinkLeftEye.Begin();
        sbBlinkRightEye.Begin();
    }
    );

上面代码片段中的第一行很简单:他们从XAML中找到并实例化故事板变量。接下来,我们有可观察的。ObserveOnDispatcher方法,稍后我会解释。然后是重要的部分:可观察的. interval (timespane . frommilliseconds(2000))。这段代码返回一个可观察序列,该序列在每个周期之后(在本例中为每2秒)生成一个值。如果你认为“这是个计时器”,那你就完全正确了。它是一个计时器,我们用它作为计时器。请注意,这已经是Rx框架提供的一个新特性。因此,虽然您可以使用DispatcherTimer或其他内置的。net计时器,但您现在有了新的可观察对象。间隔法执行相同的任务。但是,使用可观察序列的好处是,您将在后面看到,您可以使用LINQ来操作序列的生成方式。 上面代码示例中的最后一行告诉应用程序,每当可观察序列产生一个值时,就启动闪烁的故事板。也就是说,每2秒钟,我们丑陋的脸就会眨一次眼。记住可观察的。上面ObserveOnDispatcher行吗?使用这个方法是为了使我们在访问故事板对象时不会得到错误的线程异常(它是在与计时器线程不同的线程中创建的)。 收集数据 连同MainWindow.xaml。cs,你会看到一个私有类ElementAndPoint,你可能想知道它为什么在那里。它只是一个POCO(普通的CLR对象),它将帮助我们在移动鼠标和按/释放鼠标按钮时存储关于控件和点的信息。在下一节中,您将更清楚地看到这一点。 隐藏,复制Code

/// <summary/>
/// We use this private class just to gather data about the control and the point
/// affected by mouse events
/// </summary/>
private class ElementAndPoint
{
    public ElementAndPoint(FrameworkElement element, Point point)
    {
        this.Element = element;
        this.Point = point;
    }

    public FrameworkElement Element { get; set; }
    public Point Point { get; set; }
}

从事件序列 现在我们面临一种新的Rx方法:Observable.FromEvent。该方法返回一个可观察序列,其中包含底层。net事件的值。也就是说,我们告诉应用程序从MouseMove和MouseUp事件创建可观察序列,值和序列是由GetPosition函数: 隐藏,复制Code

//Create 2 observable sequences from mouse events
//targeting the MainWindow
var mouseMove = Observable.FromEvent(this, "MouseMove").Select(
                e => new ElementAndPoint(null, e.EventArgs.GetPosition(mainCanvas)));
var mouseUp = Observable.FromEvent(this, "MouseUp").Select(
              e => new ElementAndPoint(null, e.EventArgs.GetPosition(mainCanvas)));

让我们仔细看看这几行: 这个可观察的部分告诉应用程序从MouseMove事件中创建一个可观察的MouseEventArgs序列,以当前窗口(this)作为目标元素。该指令本身将返回一个MouseEventArgs值序列,但在本例中,我们将修改序列值类型,通过使用Select方法为序列中的每个值返回一个新的ElementAndPoint对象。基本上,我们说元素是空的(也就是说,我们不关心元素),点是鼠标移动时相对于mainCanvas元素的位置。fromevent&mouseup使用了相同的逻辑,但是在本例中,我们必须小心地将源类型定义为MouseButtonEventArgs,这是MouseUp事件返回的类型。 接下来的两行还定义了两个不同事件的可观察序列:MouseEnter和MouseLeave。每当您在网格界面区域(由grdFace元素分隔)中输入鼠标时,第一个序列将生成单个值。当你离开这个区域时,第二个序列产生一个值。同样,我将在后面解释如何使用这些序列。 隐藏,复制Code

//Create 2 observable sequences from mouse events
//targeting the face grid
var mouseEnterFace = Observable.FromEvent(grdFace, "MouseEnter").Select(
                     e => new ElementAndPoint(null, e.EventArgs.GetPosition(mainCanvas)));
var mouseLeaveFace = Observable.FromEvent(grdFace, "MouseLeave").Select(
                     e => new ElementAndPoint(null, e.EventArgs.GetPosition(mainCanvas)));

使用更复杂的查询 然后是我们创建一个用户控件列表来定义面部部分(眼睛,眉毛,鼻子,嘴巴): 隐藏,复制Code

//We store a list of user controls (representing portions of the face)
//so that we can create new observable events and 
//subscribe to them independently
var controlList = new List();
controlList.Add(ucLeftEyeBrow);
controlList.Add(ucLeftEye);
controlList.Add(ucRightEyeBrow);
controlList.Add(ucRightEye);
controlList.Add(ucNose);
controlList.Add(ucMouth);

一旦我们有了这个列表,我们可以很容易地迭代它们的元素来创建可观察序列的事件,这些面向部分: 隐藏,复制Code

foreach (var uc in controlList)
{
    //Initialize each user control with
    //predefined Canvas attached properties.
    Canvas.SetZIndex(uc, 1);
    Canvas.SetLeft(uc, 0);
    Canvas.SetTop(uc, 0);
    . . .

现在我们在用户控件列表上迭代,我们根据MouseDown和MouseUp UI事件创建可观察序列。还要注意,我们使用Select方法返回一个ElementAndPoint对象序列,其中包含(FrameworkElement)e。发送方值作为元素。换句话说,序列中的每个值现在有: 鼠标按钮被按下或释放的位置,鼠标向下/向上事件发生的位置 隐藏,复制Code

//Create 2 observable sequence from mouse events
//targetting the current user control
var mouseDownControl = Observable.FromEvent(uc, "MouseDown").Select(
  e => new ElementAndPoint((FrameworkElement)e.Sender, e.EventArgs.GetPosition(mainCanvas)));
var mouseUpControl = Observable.FromEvent(uc, "MouseUp").Select(
  e => new ElementAndPoint((FrameworkElement)e.Sender, e.EventArgs.GetPosition(mainCanvas)));

刚开始的时候,它的语法可能看起来有点奇怪,但是我相信,如果你用这个小例子来练习,你会习惯它的。 应用程序中的另一个重要部分是拖放功能。每个面部分都可以被拖放,这基本上是由两段代码完成的:第一部分是LINQ查询,它创建了一个可观察序列,当面部分被拖放时,该序列被填充。而第二段代码订阅该可观察序列并相应地移动面部部分: 隐藏,复制Code

//Create a observable sequence that starts producing values
//when the mouse button is down over a user control, and stops producing values
//once the mouse button is up over that control,
//while gathering information about mouse movements in the process.
var osDragging = from mDown in mouseDownControl
                 from mMove in mouseMove.StartWith(mDown).TakeUntil(mouseUp)
                 .Let(mm => mm.Zip(mm.Skip(1), (prev, cur) =>
                     new
                     {
                         element = mDown.Element,
                         point = cur.Point,
                         deltaX = cur.Point.X - prev.Point.X,
                         deltaY = cur.Point.Y - prev.Point.Y
                     }
                 ))
                 select mMove;

//Subscribe to the dragging sequence, using the information to
//move the user control around the canvas.
osDragging.Subscribe(e =>
    {
        Canvas.SetLeft(e.element, Canvas.GetLeft(e.element) + e.deltaX);
        Canvas.SetTop(e.element, Canvas.GetTop(e.element) + e.deltaY);
    }
);

上面的代码片段可以用浅显的英语翻译成:“当用户按下鼠标按钮了一些元素,而用户没有发布按钮,当用户移动鼠标在当前窗口,返回的序列值包含元素被拖动,鼠标指针位于,和代表坐标增量运动自最后一次鼠标移动。对于每个返回的值,根据计算得到的X, Y增量移动受影响元素的X, Y坐标。很容易,不是吗? 现在让我们仔细看看我们刚刚做了什么: 上述LINQ查询的核心是mouseMove可观察序列(我们在前面声明过)。StartWith和TakeUntil方法告诉应用程序观察序列必须开始/停止产生值的时间。zip (mm.Skip(1), (prev, cur)部分是一条将两个序列值合并为单个序列值的指令:这非常方便,因为它使我们能够使用前一个序列值和当前序列值,并将它们组合起来计算增量。匿名类型以new {element…修改返回的类型,以便我们可以获得关于拖放操作的更多信息。Subscribe方法描述了每次拖动面部部分时执行的操作。在我们的例子中,设置了该元素的左侧和顶部属性,因此可以移动该元素。 如你所愿订阅 继续下一部分:假设我们想让选中的部分移动到屏幕上其他元素的上方:在本例中,我们可以将ZIndex设置为一个高度值,比如100。然后我们要做的就是订阅另一个操作到mouseDownControl的观察序列,并修改元素的属性: 隐藏,复制Code

...

var mouseDownControl = Observable.FromEvent(uc, "MouseDown").Select(
    e => new ElementAndPoint((FrameworkElement)e.Sender, 
    e.EventArgs.GetPosition(mainCanvas)));

...

//Once the mouse button is up, the ZIndex is set to 100, that is,
//we want to make the user control to move on top of any other controls
//on the screen.
mouseDownControl.Subscribe(e =>
    {
        Canvas.SetZIndex(e.Element, 100);
    }
);

使用相同的技术,我们可以在用户释放元素时将其放入正确的ZIndex值。在我们的例子中,这使得眼球停留在眼睑后面。我们通过订阅mouseUpControl序列来做到这一点: 隐藏,复制Code

    ...

    var mouseUpControl = Observable.FromEvent(uc, "MouseUp").Select(
            e => new ElementAndPoint((FrameworkElement)e.Sender, 
            e.EventArgs.GetPosition(mainCanvas)));

    ...

    //Once the mouse button is down, the ZIndex is set to the proper value (1),
    //unless for the eye controls, which are set to -1 in order to put them
    //behind the face.
    mouseUpControl.Subscribe(e =>
        {
            switch (e.Element.Name)
            {
                case "ucLeftEye":
                case "ucRightEye":
                    Canvas.SetZIndex(e.Element, -1);
                    break;
                default:
                    Canvas.SetZIndex(e.Element, 1);
                    break;
            }
        }
    );
}

完成的脸 最后,我们订阅了mouseMove observable序列。注意这里有很多东西在动:眉毛在动,眼睛在看着鼠标光标,牙齿在上下移动。我们美丽丑陋的脸已经完成,注意我们的鼠标移动。 当然,我们可以使用单独的动作,甚至单独的函数。用它更好地服务于你。 隐藏,收缩,复制Code

var leftPupilCenter = new Point(60, 110);
var rightPupilCenter = new Point(130, 110);

//Subscribe to the mousemove event on the MainWindow. This is used
//to move eyes and eyebrows.
mouseMove.Subscribe(e =>
{
    double leftDeltaX = e.Point.X - leftPupilCenter.X;
    double leftDeltaY = e.Point.Y - leftPupilCenter.Y;
    var leftH = Math.Sqrt(Math.Pow(leftDeltaY, 2.0) + Math.Pow(leftDeltaX, 2.0));
    var leftSin = leftDeltaY / leftH;
    var leftCos = leftDeltaX / leftH;

    double rightDeltaX = e.Point.X - rightPupilCenter.X;
    double rightDeltaY = e.Point.Y - rightPupilCenter.Y;
    var rightH = Math.Sqrt(Math.Pow(rightDeltaY, 2.0) + Math.Pow(rightDeltaX, 2.0));
    var rightSin = rightDeltaY / rightH;
    var rightCos = rightDeltaX / rightH;

    if (!double.IsNaN(leftCos) &&
        !double.IsNaN(leftSin))
    {
        ucLeftEye.grdLeftPupil.Margin = 
           new Thickness(leftCos * 16.0, leftSin * 16.0, 0, 0);
    }

    if (!double.IsNaN(rightCos) &&
        !double.IsNaN(rightSin))
    {
        ucRightEye.grdRightPupil.Margin = 
           new Thickness(rightCos * 16.0, rightSin * 16.0, 0, 0);
    }

        var distFromFaceCenter = Math.Sqrt(Math.Pow(e.Point.X - 90.0, 2.0) + 
                                 Math.Pow(e.Point.Y - 169.0, 2.0));

        ucLeftEyeBrow.rotateLeftEyebrow.Angle = -10 + 10 * (distFromFaceCenter / 90.0);
        ucRightEyeBrow.rotateRightEyebrow.Angle = 10 - 10 * (distFromFaceCenter / 90.0);

        ucMouth.pnlTeeth.Margin = 
          new Thickness(0, 10 * (distFromFaceCenter / 90.0) % 15, 0, 0);
    }
);

最后考虑 正如我之前所说的,这只是对Rx功能的一瞥。当然,响应式扩展还可以做更多的工作,但是如果本文在某种程度上对您有用,我将非常高兴。要了解更多关于Rx的方法,请在代码项目中阅读其他优秀的Rx文章: 有趣的Rx探索反应扩展(Rx)通过Twitter和Bing地图通过示例mashup的Rx框架 历史 2011-02-27:初始版本。 本文转载于:http://www.diyabc.com/frontweb/news29898.html

你可能感兴趣的:(使用响应扩展的响应面(Rx))