- 鼠标跟随。app会遇到这么种需要,当鼠标是拖着离开A控件,而且希望把后面的鼠标事件(motion、up)导向A,这种情形称为鼠标正跟随A控件,简称鼠标跟随,相应地,控件A称为鼠标跟随控件。举个例子,正在编写一个通过拨针转圈方法、然后修改时间的钟盘控件,按下手指开始拨针了,某一刻手指离开钟盘所在矩形,但只要还是做出转圈手势,那还是要能操作钟盘。
tdistributor内3个和mouse相关变量
变量 | 类型 | 何时设置、清除 | 说明 |
mouse_focus_ | twidget* | mouse_enter时置有效值。mouse_leave或post_mouse_up时置nullptr。 | 指示是哪个控件正获得鼠标焦点。如果mouse_captured_ 是true时,它就是鼠标跟随控件。 |
mouse_click_ | twidget* | SDL_BUTTON_LEFT时置有效值。鼠标离开了mouse_click_或post_mouse_up时置nullptr。 | 指示一旦松开鼠标,哪控件会发送单击(xx_BUTTON_CLICK)/双击(xx_BUTTON_DOUBLE_CLICK)事件 |
mouse_captured_ | bool | app调用了capture_mouse(widget)时置true。mouse_leave或post_mouse_up是置false。 | 是否有控件正要求鼠标跟随 |
mouse_focus_指示哪个控件正获得焦点,并表示当前正发生鼠标跟随,只有mouse_captured_就true,才发表发生跟随,当然,发生跟随时mouse_focus_一定不是nullptr。
事件系统会保证遵守影响app代码的规范
- (tdistributor)enter、leave会配对发送,
- (tdistributor)down、up之前应该有motion到过该坐标。由于无法做到down、up严格配对,down、up处理例程要能容许连续收到多个down或多个up。
- (tdistributor)只有松开时鼠标落在的是一个有效坐标,才会发送up,up事件所带的坐标参数总是个有效坐标。因意外离开时,像拖着鼠标离开app窗口,会发送leave,而不会发送up。
- (tdistributor)处于鼠标跟随状态时,当鼠标离开鼠标跟随控件所在矩形,不会发送leave,自然也不会发送enter。只有当松开鼠标,或发生意外事件,像离开app窗口,才会发送leave。
- (caller)若控件支持拖拽操作,建议在down/leave管理first_coordinate_。即在down设置拖拽起始坐标,在leave清除first_coordinate_,结束拖拽。
- 各控件应该严格按tdistributor定义的规则处理motion、down、up、enter、leave。若想主动结束拖拽,应该调用twindow提供的release_mouse_focus方法,而不是私自清除first_coordinate_。一旦这么做,极可能造成控件的拖拽状态和tdistributor中的三变量不一致。
一、tdistributor
可把控件事件分为三类,一是和“原生”对应的WHEEL、DOWN、UP、MOTION,二是进入和离开控件,三是单击和双击。后面两种从原生衍生出来,这意味着一个操作可能会产生多个事件,这时发送事件的顺序是先原生,后enter/leave,最后是单击/双击。举个例子,一次鼠标松开会导致需要发原生的up、leave、以及click,这时是先发up,后leave,最后click。同理,一次motion可能导致原生motion,离开一个控件的leave,进入另一个控件的enter,这时发送次序是motion、leave、enter。
代码保证控件会收到配对的enter、leave,也就是说,控件A收到了enter,那后面一定会有leave。这个规则对实现拖拽鼠标会较有用,拖拽往往从down开始,leave结束,过程中通过motion知道鼠标在哪里。为什么是leave,而不是up?——只有松开时鼠标落在的是一个有效坐标,才会发送up,因为up所带的坐标参数要求是个有效坐标。因意外离开时,像离开app窗口,此时会发送leave,但不会发送up。
鼠标离开app窗口时,只会发一个坐标是(-999999,-1)的leave,不会发其它事件,像motion、up。为方便app判断是否是(-999999,-1),提供了叫is_mouse_leave_window_event的宏。
处于鼠标跟随状态时,当鼠标离开鼠标跟随控件所在矩形,不会发送leave,自然也不会发送enter。只有当松开鼠标,或发生意外事件,像离开app窗口,才会发送leave。
鼠标从down到up都没离开过一个控件(mouse_click_),那就会在该控件产生一个单击事件(click)。什么时候发送double_click?之前已经发过一次click,此次up又可以导致发送一次click,而这两次click间隔小于double_click_time,那第二次click会变成发送double_click。这意味着,在发送double_click前一定已经发了一次click。哪里设置double_click_time?gui.dat中settings块的double_click_time字段,当前值是500(毫秒)。
处理控件事件时,Rose允许在LEFT_BUTTON_CLICK、LEFT_BUTTON_DOUBLE_CLICK、RIGHT_BUTTON_UP弹出新窗口,其它事件要弹出新窗口的,须使用“3.8.4”小节介绍的Post+app_OnMessage机制。
mouse_focus_、mouse_captured_共同实现鼠标跟随。要鼠标跟随了,首先是有人调用twindow::mouse_capture,在实现函数时,tdistributor把当前mouse_focus_或参数指定的控件设为mouse_focus_,同时mouse_captured_设为true。只要鼠标不松开/离开窗口,后续鼠标事件都会自动导向这个控件。要注意,它不影响click,click要求是鼠标从down到up都没离开过该控件。什么时候mouse_foucs_回到nullptr?——松开鼠标或鼠标离开窗口就会置nullptr。
motion、down、motion、up,鼠标总是这么周而复始地操作,但可以这么认为,up是个复位点。一旦收到up,代码要恢复到初始状态,像会发mouse_leave,mouse_focus_、focus_要置nullptr。复位点除了up,还有一个是弹出新窗口B,之前窗口A会进入初始状态。此种情况要注意,移动鼠标关闭B(弹式出菜单)时,如果立即按下鼠标,由于坐标没变过,tevent_handle不会发额外motion,A的mouse_focus_将是初始状态时nullptr!目前处理是忽略这个down。
不要奢望tdistributor能够精确统计down、up,并进行严格检查。举个例子,用一个叫is_down_布尔变量指示是否正按下鼠标,于是收到down时,诊断该变量必须是false,然后置true;收到up时,诊断该变量必须是true,然后置false。愿望美好,很难实现。在PC,鼠标会离开窗口,分拖着鼠标离开还是不拖着离开;回到窗口,也要分拖着和不拖着。除离开,窗口失去焦点、获得焦点、最小化也会产生影响。而且目前测试下来,SDL事件行为在不同平台不完全一致,像Andoird,横、竖屏切换时会收到离开窗口事件。为此,如果真想做到down、up配对,最好等SDL严格统一了各平台上事件模型。
OUT_OF_CHAIN。以上事件只能发向鼠标击中的、以及父控件组成的控件链,有时要发向这之外的控件。举个例子,长按列表行后弹出菜单,用户可能是在列表之外的地方按下鼠标,这时也该能收到这鼠标按下事件,以便关闭菜单。OUT_OF_CHAIN第五个参数指示发生了什么事件,包括LEFT/MIDDLE/RIGHT_BUTTON_DOWN、WHEEL_DOWN/UP/LEFT/UP。
RECEIVE_KEYBOARD_FOCUS/ LOSE_KEYBOARD_FOCUS。对键盘焦点有兴趣的控件(目前只有text_box)需处理得到焦点、失去焦点事件。对失去焦点,第6个参数extra_widget指示了下个要获得焦点的控件,text_box可用它判断此次失去焦点时要不要关软键盘。这两个事件不发源于SDL,由app主动调用twindow::keyboard_capture产生。
各控件应该严格按tdistributor定义的规则处理motion、down、up、enter、leave。若想主动结束拖拽,应该调用twindow提供的release_mouse_focus方法,而不是私自清除first_coordinate_。一旦这么做,极可能造成控件的拖拽状态和tdistributor中的三变量不一致。
二、tscroll_container(滚动控件)
2.1 滚动控件的嵌套问题
列表控件上有个滚动面板控件。
- 鼠标向左移动超过一定距离后,列表会置上drag_at_ = row。
- 下一次mouse_motion到来了,滚动控件调用scrollbar_moved,并要重设所有孩子的视区,这当中自然包括列表控件。这会调用tlistbox::mini_set_content_grid_origin,这函数要确保drag_at_是twidget::npos,导致抛出诊断异常!
如何解决这问题?——当前是窗口脚本时就禁止出现滚动控件嵌套。
2.2 captured_mouse_can_to_scroll_container
captured_mouse_can_to_scroll_container是twidget的虚方法,用于表示当“我”是鼠标跟随控件时,是否可以把跟随控件变更到“我”父控件scroll_container的content_grid_。值默认是false,即不允许变更。目前只有button、有些情况下的text_box允许变更。为什么scroll_container要拿到鼠标跟随?——当拖着鼠标离开自已时,还能继续挪动它的内容网格。

控件 | captured_mouse_can_to_scroll_container | |
button | true | 报表列表控件单元是button时,要支持挪动,参考图1报表 |
track | false | 原则是不能track出现在滚动控件中 |
text_box | true/false | (true)进入选择状态后,为完成操作,编辑框需要获得“一次”完整鼠标事件/(false)不在选择状态 |
scrollbar | false | |
scroll_container(实际是content_grid_) | false |
注1:为什么滚动控件把鼠标跟随挂接向content_grid_,而不是content_或tscroll_container
content_ | 它不是content_grid_中控件的父控件,将不能接收那些控件的事件。可以排除。 |
content_grid_ | 缺点:当content_grid_不能占据整个content_,将不能接收消息。 |
tscroll_container | 缺点:当scroll_container存在除content_grid_外控件,像列表中的左滑菜单grid,为支持下拉刷新引入的track,在这些控件发生的事件也会导向scroll_container。而这些事件增加干扰,使得逻辑变得复杂。在隶属关系上,附加控件和content_grid_互不关联,但它们会是scroll_container的子控件。 |
考虑到附加控件多样性,那会使得逻辑变得复杂,选择content_grid_。至于当content_grid_不能占据整个content_时,如何在空白处接收事件再做专门处理。
2.3 设置、清除first_coordinate_
和track控件一样,滚动控件也在是收到left_button_down时设置first_coordinate_。
对清除first_coordinate_,track控件是个鼠标跟随控件,因而只有当松开鼠标,或发生意外事件,像离开app窗口,才会发送leave。于是在收到leave时,track清除first_coordinate_,即结束拖拽。对滚动控件,它是一个容器控件,尤其不处在鼠标跟随时,收到leave,可能只是从内中的一个控件进入到另一个控件。于是如何清除first_coordinate_,会可能发生在up、leave。
void tscroll_container::signal_handler_left_button_up(const tpoint& coordinate, bool pre_child) { set_first_coordinate_null(coordinate); }
up负责清除正常松开鼠标时场景。

void tscroll_container::signal_handler_mouse_leave(const tpoint& coordinate, const tpoint& coordinate2, bool pre_child) { .... // tscroll_container是先收到原生up,再是此处的leave,因而first_coordinate_可能已经被清除 if (is_null_coordinate(first_coordinate_)) { return; } if (is_magic_coordinate(coordinate) || (window->mouse_captured_widget() != content_grid_ && !window->point_in_normal_widget(coordinate.x, coordinate.y, *content_))) { set_first_coordinate_null(coordinate); } .... }
- is_magic_coordinate(coordinate)是true,意味着这不是鼠标正常松开、而是意外事件而导致的leave。
- window->mouse_captured_widget() != content_grid_ && !window->point_in_normal_widget(coordinate.x, coordinate.y, *content_)。这是种什么场景,让结合图2。一直按着鼠标,没有发生鼠标跟随,鼠标落在label控件“人员注册”,然后鼠标离开了它,这时会收到leave。虽然鼠标离开了“人员注册”,但只要在滚动控件内,这个leave就不应该认为结束拖拽。只有当鼠标离开了滚动控件的内容网格content_grid_,即“!window->point_in_normal_widget(coordinate.x, coordinate.y, *content_)”,才会认为结束。注:如果coordinate正落在此个滚动控件的滚动条,point_in_normal_widget也是返回true。
总的来说,up负责清除正常松开鼠标时场景。leave负责的分为两类,一是发生了异常事件,像离开app窗口;二是没有跟随控件或跟随控件不是content_grid_时,拖着鼠标离开滚动控件的内容网格。
2.4 挪动滚动控件
使用的示例:scroll_panel中有一个listbox、一个report,report中有button。listbox不可挪动,report可挪动。
- 当x、y有满足门限的偏移时,就会试图把captured_mouse切到content_grid_。为什么scroll_container要拿到captured_mouse?为了当拖着鼠标离开自已时,还能继续挪动它的内容网格。
- 处理挪动的代码放在MOUSE_MOTION处理例程,挂接向child、post_child两个集合。为什么选择post_child而不是pre_child?是为了后面的设置captured_mouse首先拿到最内层滚动控件。
- 必须把captured_mouse设置在最内层滚动控件的content_grid_。原因是一旦设了captured_mouse,后续的消息只能发给它和它的那些父窗口。——让回看实例,在button按下鼠标,由于scroll_panel、report都是button的父控件,它们的post_child都要处理这down事件,具体是把first_coordinate_设为有效值,button则获得captured_mouse_。当移动鼠标超过一定距离时,如果把captured_mouse_设到scroll_panel那会导致什么结果?由于scroll_panel是最外层控件,接下mouse、up都将传给它,而不会继续传给它内里的report,这将导致report的first_coordinate处于混乱状态!
- 一次只能挪动一个滚动控件。这有两个方面。1)当正挪动的内层控动件是可以移动时,那就要阻止外层的scroll_panel,阻止的方法是让此次MOUSE_MOTION把halt设为true(这个方法会导致后面的scroll_panel收不到MOUSE_MOTION,不知将来会不会有问题?)。2)当正挪动的内层控动件是不可以移动时,不执行MOUSE_MOTION后半部分的处理代码。
scroll_container的can_scroll指示着它是否可以滚动。当前是由外界调用set_can_scroll设置,理论上应该是由tscroll_container::place自动设置。