天天看点

16.4.3 使用消息进行通信

16.4.3 使用消息进行通信

    清单 16.13 创建一个邮箱处理器,statee,类型为 MailboxProcessor<DrawingMessage>。注意,我们创建的 state 方法,是同名的非泛型类的一个成员。我们在 C# 中用来进行类型推断的就是同样的这种模式,在开始将其集成到我们的代码之前,会看到邮箱处理器支持哪些操作。表 16.2 显示了这个泛型类型最重要的实例方法。

Table 16.2 The most important methods provided by the MailboxProcessor<'Msg> type

邮箱函数 函数的描述
Post 发送消息到邮箱处理器,而无需等待任何回复。如果邮箱处理器忙,消息会保存在队列中。
PostAndReply 发送一条 AsyncReplyChannel <'T> 参数的消息,到邮箱处理器,阻塞调用线程,直到邮箱处理器调用通道的 Reply 方,然后,处理器返回值,发送给通道。
PostAndAsyncReply 类似于 PostAndReply,但它是以异步方式运行的。它通常是从异步工作流中,使用 let! 调用的。因此,在消息等待进行处理期间,不阻止调用线程。
Receive 当创建邮箱处理器,异步地从队列中接收下一条消息时,我们使用这个方法,这样可以在工作流内对其进行处理。在邮箱处理器之外,不应使用此方法。
Scan 像 Receive 一样,这个方法不应该用在邮箱处理器以外,当处理器处于无法处理所有类型的消息的状态时,它可以使用。提供的 lambda 函数返回一个选项类型,当包含异步工作流处理消息时,它可以返回 Some,不能处理消息时,返回 None。未处理的消息保留在队列中,供以后处理。

    我们已经看到如何使用 Receive,稍后将谈论 Scan,余下的三个方法可以从任何线程使用。虽然处理器可以将消息发送到自身,有时很有用,但是,更典型的情况是,消息从不同的过程(例如,工作异步流实现图形用户界面的交互,或者后台的工作线程),发送给处理器。

改进绘图进程

    现在我们知道什么是可用的,可以改变绘图进程,将保存用户已绘制的所有矩形,并允许用户选择不同的颜色。首先,我们需要更改绘图代码。原本 drawRectangle 函数会擦除整个屏幕,如果我们想要绘制多个矩形,这不是适当的。我们仍需要清除屏幕,但只是一次,而不是为每个矩形。清单 16.14 显示所有了一新函数,它绘制指定列表中的所有矩形。drawRectangle 函数没有显示,唯一的改变只是删除第一个调用 Clear。

Listing 16.14 Utility function for drawing rectangles (F#)

let redrawWindow(rectangles) =

  use gr = form.CreateGraphics()

  gr.Clear(Color.White)

  for r in rectangles do

    drawRectangle(r)

    这个函数清除窗体的内容,然后,遍历给定列表中的所有元素,使用 drawRectangle 绘制每个单独的矩形。列表中保存的矩形,是有三个元素(颜色和两个对角)的元组,兼容于 drawRectangle 函数所预期的元组。

    现在,我们终于准备修改处理用户交互的进程。因为整个代码实现为一个异步工作流,我们想从保存状态的邮箱处理器信息时,可以使用异步方法 PostAndAsyncReply 。当可以使用它时,这是首选方案,因为它不会阻止调用线程。清单 16.15 中的大部分的代码与清单  16.11 是相同的,所以,我们已经突出显示已更改的行。

Listing 16.15 Changes in the drawing process (F#)

let rec drawingLoop(clr, from) = async {

  let! move = Async. AwaitObservable(form.MouseMove)

  if (move.Button &&& MouseButtons.Left) = MouseButtons.Left then

    let! rects = state.PostAndAsyncReply(GetRectangles)

    redrawWindow(rects)

    drawRectangle(clr, from, (move.X, move.Y))

    return! drawingLoop(clr, from)

  else

    return (move.X, move.Y) }

let waitingLoop() = async {

  while true do

    let! down = Async. AwaitObservable(form.MouseDown)

    let downPos = (down.X, down.Y)

    if (down.Button &&& MouseButtons.Left) = MouseButtons.Left then

      let! clr = state.PostAndAsyncReply(GetColor)

      let! upPos = drawingLoop(clr, downPos)

      state.Post(AddRectangle(clr, downPos, upPos)) }

    让我们首先看一下在 drawingLoop 函数中所做的改变,在这里,我们更新了窗口。最初,在绘制新的矩形之前,需要擦除所有的内容,但现在,还需要绘制所有的、早先已经绘制的矩形。我们从邮箱处理器获得矩形的列表,通过发送 GetRectangles 消息。这个消息有一个类型 AsyncReplyChannel<'T> 的参数,将用于邮箱处理器回复调用者,但我们在代码中没有显式指定通道。F# 编译器会将这个差别联合的构造函数(GetRectangles),看作是只有一个参数的函数。我们可以写同样的事情,像这样:

let! rects = state.PostAndAsyncReply(fun chnl -> GetRectangles(chnl))

    如果以这种更长的形式写代码,可以很容易看到细节。PostAndAsyncReply 方法为回复创建了一个通道,并使用指定的 lambda 函数创建拾这个通道的消息。邮件然后发送能邮箱处理器,然后,工作流被挂起,直到回复发送给这个通道。一旦我们收到有矩形列表的回复,就可以绘制了。然后,立刻绘制用户正在绘制的新矩形。注意,回复可以从后台线程发送。完成后,PostAndAsyncReply 方法返回给调用者线程,所以,工作流的其余部分将在图形用户界面线程上执行。

    第二个变化是在 waitingLoop 函数中,在这里,用户开始绘制新的矩形。首先,我们读当前所选的颜色。我们还没有实现用户界面,选择不同的颜色,但是,这方面很快就实现,所以,我们或许也有准备。对 AwaitObservable 的调用完成后,必须读取颜色;否则,用户可能在我们取到颜色后,开始绘图前,更改颜色。一旦我们有了这个颜色,就可以调用 drawingLoop 函数,处理当用户一直按下鼠标按钮的时间段。我们使用 Post 方法发送有关新创建矩形的所有信息,给邮箱处理器。

添加用户界面

    应用程序的用户界面将会相当简单,但我们会需要从不同的地方调用该邮箱处理器,处理应用程序的当前状态。首先,我们将添加 Paint 事件的处理程序,所以,当窗口的任何的一部分无效时,应用程序就重绘矩形。这会发生在应用程序调整大小,或者其他的窗口移动它的前面。其次,我们添加有一个按钮的工具栏,用户可以更改当前的颜色。图 16.6 显示了运行中的应用程序。

16.4.3 使用消息进行通信

图 16.6 运行中的应用程序,只包含绘制的矩形。

    清单 16.16 显示应用程序其余部分最有趣的代码。我们省略了创建工具栏和按钮代码,但在本书的网站上有完整源代码。

Listing 16.16 Implementing the user interface (F#)

let btnColor = new ToolStripButton(...)

btnColor.Click.Add(fun _ –>

  use dlg = new ColorDialog()

  if (dlg.ShowDialog() = DialogResult.OK) then

    state.Post(SetColor(dlg.Color)) )

form.Paint.Add(fun _ –>

  let rects = state.PostAndReply(GetRectangles)

  redrawWindow(rects) )

[<STAThread>]

do

  Async.StartImmediate(waitingLoop())

  Application.Run(form)

    大部分代码是相当简单的。我们创建用户界面,并注册两个事件处理程序。我们不需要用这些事件做任何复杂的事情,所以,只是调用 Add 方法,而没有使用 Observable 模块的函数。第一个处理程序显示 ColorDialog,以便用户可以选择新的颜色。如果选择了一种颜色,发布有新颜色的消息给邮箱处理器。我们不需要等待任何对这个消息的回复,所以,这个操作的执行不会阻止线程。

    第二个事件处理程序是为 Paint 事件。首先,要获取已有矩形的列表,使用 PostAndReply 方法实现的。它构造的消息有回复通道,然后等待,直到邮箱发送回复。这个方法阻塞了线程,因此,只有当无法以异步方式完成操作时,才应使用。请求更新基于 Windows Forms 的应用程序窗口,肯定是这些情况中的一种,所以,这种用法是正确的。

    到目前为止,我们一直在直接使用邮箱处理器对象。在开发的早期阶段,这是好的,但一旦应用程序变得更大,或如果我们想要把应用程序的一部分到单独的库,把邮箱处理器封装在对象中,会更好。虽然我们不打算更进一步扩展绘图应用程序,但是,看一下它包含什么,还是值得的。

继续阅读