拥有实际的小部件对象是非常面向对象的。函数世界中常用的技术是使用函数响应式编程 (FRP)。我将简要概述纯 Haskell 中的小部件库在使用 FRP 时的外观。
tl/dr:您不处理“小部件对象”,而是处理“事件流”的集合,而不关心来自哪些小部件或这些流来自何处。
在 FRP 中,有一个基本概念Event a
,可以看作是一个无限列表[(Time, a)]
。所以,如果你想为一个向上计数的计数器建模,你可以把它写成[(00:01, 1), (00:02, 4), (00.03, 7), ...]
,它将一个特定的计数器值与给定的时间相关联。如果你想模拟一个被按下的按钮,你可以生成一个[(00:01, ButtonPressed), (00:02, ButtonReleased), ...]
通常还有一种叫做 a 的东西Signal a
,它类似于 a Event a
,只是模型值是连续的。您在特定时间没有一组离散的值,但是您可以询问Signal
它的值,比如说,00:02:231
它会给您这个值4.754
或其他东西。将信号视为模拟信号,例如医院的心脏电荷计(心电图设备/Holter 监视器)上的信号:它是一条上下跳跃但从不产生“间隙”的连续线。例如,一个窗口总是有一个标题(但也许它是空字符串),所以你总是可以询问它的值。
在 GUI 库中,在低级别上,会有一个mouseMovement :: Event (Int, Int)
和mouseAction :: Event (MouseButton, MouseAction)
或一些东西。这mouseMovement
是实际的 USB/PS2 鼠标输出,因此您只能将位置差异作为事件获得(例如,当用户向上移动鼠标时,您会收到事件(12:35:235, (0, -5))
。然后您就可以“整合”或更确切地说“累积”获得mousePosition :: Signal (Int, Int)
绝对鼠标坐标的移动事件。mousePosition
还可以考虑绝对指针设备,例如触摸屏,或重新定位鼠标光标的操作系统事件等。
类似地,对于键盘,会有keyboardAction :: Event (Key, Action)
, 并且还可以将该事件流“集成”到 akeyboardState :: Signal (Key -> KeyState)
中,让您可以在任何时间点读取键的状态。
当您想在屏幕上绘制内容并与小部件交互时,事情会变得更加复杂。
要只创建一个窗口,可以使用一个名为“魔术函数”:
window :: Event DrawCommand -> Signal WindowIcon -> Signal WindowTitle -> ...
-> FRP (Event (Int, Int) {- mouse events -},
Event (Key, Action) {- key events -},
...)
该函数会很神奇,因为它必须调用特定于操作系统的函数并创建一个窗口(除非操作系统本身是 FRP,但我对此表示怀疑)。这也是它在monad 中的原因,因为它会在幕后FRP
调用monad 中的createWindow
andsetTitle
等。registerKeyCallback
IO
当然可以将所有这些值分组到数据结构中,以便:
window :: WindowProperties -> ReactiveWidget
-> FRP (ReactiveWindow, ReactiveWidget)
它们是决定窗口外观和行为的WindowProperties
信号和事件(例如,是否应该有关闭按钮、标题应该是什么等)。
代表键盘和鼠标事件的ReactiveWidget
S&E,以防您想在应用程序中模拟鼠标单击,以及Event DrawCommand
代表您想在窗口上绘制的内容流。这种数据结构对所有小部件都是通用的。
ReactiveWindow
表示窗口最小化等事件,输出表示ReactiveWidget
来自外部/用户的鼠标和键盘事件。
然后创建一个实际的小部件,比如说一个按钮。它将具有以下签名:
button :: ButtonProperties -> ReactiveWidget -> (ReactiveButton, ReactiveWidget)
将ButtonProperties
确定按钮的颜色/文本/等,并且ReactiveButton
将包含例如Event ButtonAction
和Signal ButtonState
读取按钮的状态。
请注意,该button
函数是纯函数,因为它仅依赖于纯 FRP 值,例如事件和信号。
如果想要对小部件进行分组(例如水平堆叠它们),则必须创建例如:
horizontalLayout :: HLayoutProperties -> ReactiveWidget
-> (ReactiveLayout, ReactiveWidget)
将HLayoutProperties
包含有关边框大小和ReactiveWidget
所包含小部件的 s 的信息。然后将为每个子小部件ReactiveLayout
包含一个带有一个元素的元素。[ReactiveWidget]
布局会做的是它有一个内部Signal [Int]
来确定布局中每个小部件的高度。然后它将接收来自输入的所有事件ReactiveWidget
,然后根据分区布局选择一个输出ReactiveWidget
以将事件发送到,同时还将例如鼠标事件的来源转换为分区偏移量。
为了演示这个 API 是如何工作的,考虑这个程序:
main = runFRP $ do rec -- Recursive do, lets us use winInp lazily before it is defined
-- Create window:
(win, winOut) <- window winProps winInp
-- Create some arbitrary layout with our 2 widgets:
let (lay, layOut) = layout (def { widgets = [butOut, labOut] }) layInp
-- Create a button:
(but, butOut) = button butProps butInp
-- Create a label:
(lab, labOut) = label labProps labInp
-- Connect the layout input to the window output
layInp = winOut
-- Connect the layout output to the window input
winInp = layOut
-- Get the spliced input from the layout
[butInp, layInp] = layoutWidgets lay
-- "pure" is of course from Applicative Functors and indicates a constant Signal
winProps = def { title = pure "Hello, World!", size = pure (800, 600) }
butProps = def { title = pure "Click me!" }
labProps = def { text = reactiveIf
(buttonPressed but)
(pure "Button pressed") (pure "Button not pressed") }
return ()
(def
来自Data.Default
于data-default
)
这将创建一个事件图,如下所示:
Input events -> Input events ->
win ---------------------- lay ---------------------- but \
<- Draw commands etc. \ <- Draw commands etc. | | Button press ev.
\ Input events -> | V
\---------------------- lab /
<- Draw commands etc.
请注意,任何地方都不必有任何“小部件对象”。布局只是一个根据分区系统转换输入和输出事件的函数,因此您可以将获得访问权限的事件流用于小部件,或者您可以让另一个子系统完全生成流。按钮和标签也是如此:它们只是将点击事件转换为绘制命令或类似内容的函数。它是完全解耦的代表,并且本质上非常灵活。