焦點事件(Focus events)

當你需要把焦點移到某個visual element或移開時,可以使用Focus event。

Focus events分為兩種不同的類型:

  • FocusOutEventFocusInEvent:在焦點發生變化之前,焦點離開事件(FocusOutEvent)和焦點進入事件(FocusInEvent)會沿著事件傳播路徑(propagation path)發送。
  • FocusEventBlurEvent:焦點事件(FocusEvent)和失焦事件(BlurEvent)會在焦點發生變化後立即發送到事件目標(event target)
Event 描述 Trickles down Bubbles up Cancellable
FocusOutEvent 在失去焦點之前,會發送一個FocusOutEvent
FocusInEvent 在element取得焦點之前,會發送一個FocusInEvent
BlurEvent 在失去焦點之後,會發送一個BlurEvent
FocusEvent 在element取得焦點之後,會發送一個FocusEvent

專屬於Focus Event的屬性

  • relatedTarget:在焦點事件中參與的第二個target element。
    • 對於FocusOutEventBlurEvent,這個屬性為聚焦的element。
    • 對於FocusInEventFocusEvent,這個屬性為失去焦點的element。
Event target relatedTarget
BlurEvent 失去焦點的element 取得焦點的element
FocusEvent 取得焦點的element 失去焦點的element
FocusInEvent 取得焦點的element 失去焦點的element
FocusOutEvent 失去焦點的element 取得焦點的element
例子:使用FocusInEventFocusOutEvent為TextField實現placeholder text功能
  1. 在Assets > Scripts > Editor 下建立一個 C# Script PlaceHolderExample.cs
  2. 將以下程式碼複製到剛剛建立的C# Script中
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    using UnityEditor;
    using UnityEngine;
    using UnityEngine.UIElements;

    public class PlaceHolderExample : EditorWindow
    {
    // 這個Attributes會在 Windows -> UI Tollkit 下加入一個 PlaceHolderExample
    [MenuItem("Window/UI Toolkit/PlaceHolderExample")]
    public static void ShowExample()
    {
    PlaceHolderExample wnd = GetWindow<PlaceHolderExample>();
    wnd.titleContent = new GUIContent("PlaceHolderExample");
    }

    private bool placeHolderMode = true;
    private const string placeHolderText = "Write here";

    public void CreateGUI()
    {
    // 建立 TextField
    TextField textField = new TextField();
    textField.value = placeHolderText;
    rootVisualElement.Add(textField);

    // 註冊FocusInEvent與FocusOutEvent
    textField.RegisterCallback<FocusInEvent>(OnFocusInTextField);
    textField.RegisterCallback<FocusOutEvent>(OnFocusOutTextField);
    }

    private void OnFocusInTextField(FocusInEvent evt)
    {
    // 如果這個 text field 收到聚焦,表示使用者想要輸入字串,因此要將placeholder文字清空
    if (placeHolderMode)
    {
    var textField = evt.target as TextField;
    textField.value = "";
    }
    }

    private void OnFocusOutTextField(FocusOutEvent evt)
    {
    // 如果這個 text field 失焦,表示使用者已經完成輸入,但如果text field為空字串,則要將placeholder文字填回去。
    var textField = evt.target as TextField;
    placeHolderMode = string.IsNullOrEmpty(textField.value);
    if (placeHolderMode)
    textField.value = placeHolderText;
    }
    }
  3. 在Unity編輯器中找到 Window > UI Toolkit > PlaceHolderExample

Reference: https://docs.unity3d.com/Manual/UIE-Focus-Events.html

點擊事件(Click events)

當一個Visual Element被滑鼠左鍵(或點擊裝置(pointing device)的第一個按鈕)點擊,將觸發一個Click Event。進行點擊操作的Visual Element會觸發Pointer Down EventPointer up Event。此外Click Event也可以檢測到非按鈕Visual Element的點擊,例如Toggle control便是使用了Click Event來處理顯示,隱藏勾號(check mask)並改變control的值。

Event 描述 Trickles down Bubbles up Cancellable
ClickEvent 當滑鼠左鍵點擊時,就會觸發一個Click Event
註冊Click Event
1
2
3
4
5
6
7
8
// 使用RegisterCallback註冊ClickEvent,同時還傳入一個VisualElement參數
// 這個asset是當click event被觸發之後,要關閉的element
btnClose.RegisterCallback<ClickEvent, VisualElement>(Clicked, asset);

private void Clicked(ClickEvent evt, VisualElement root)
{
root.ShowVisualElement(false);
}
例子:以下例子示範如何使用Click Event讓一個visual element在被點擊之後將原有顏色,替換為另一個新的顏色。
  1. 在Assets > Scripts > Editor 下建立一個 C# Script ClickEventExampleWindow.cs
  2. 將以下程式碼複製到剛剛建立的C# Script中
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    using UnityEditor;
    using UnityEngine;
    using UnityEngine.UIElements;

    public class ClickEventExampleWindow : EditorWindow
    {
    // 這個Attributes會在 Windows -> UI Tollkit 下加入一個 ClickEventExample
    [MenuItem("Window/UI Toolkit/ClickEventExample")]
    public static void ShowExample()
    {
    var wnd = GetWindow<ClickEventExampleWindow>();
    wnd.titleContent = new GUIContent("Click Event Example");
    }

    public void CreateGUI()
    {
    // 建立四個不同顏色的box
    for (int i = 0; i < 4; i++)
    {
    // 為這些Visual Element設定隨機的background顏色
    var newBox = new VisualElement() { style = { flexGrow = 1, backgroundColor = GetRandomColor() } };
    rootVisualElement.Add(newBox);

    // 為這個Element註冊一個ClickEvent callback
    newBox.RegisterCallback<ClickEvent>(OnBoxClicked);
    }
    }

    // 這個callback會改變target的Background color。
    private void OnBoxClicked(ClickEvent evt)
    {
    // 只在taget階段執行這個callback
    if (evt.propagationPhase != PropagationPhase.AtTarget)
    return;

    // 設定新的Background color
    var targetBox = evt.target as VisualElement;
    targetBox.style.backgroundColor = GetRandomColor();
    }

    private Color GetRandomColor()
    {
    return new Color(Random.Range(0, 1f), Random.Range(0, 1f), Random.Range(0, 1f));
    }
    }
  3. 在Unity編輯器中找到 Window > UI Toolkit > ClickEventExample
  4. 觀看結果

Reference: https://docs.unity3d.com/Manual/UIE-Click-Events.html

變更事件(Change events)

當一個Element的值改變時,就會發送一個Change Event,例如使用者切換了(toggles)一個核取方塊(Checkbox),Change Event是一個帶有類型的事件(Typed Event),它含有這個Element先前的值和新值。由於Change Event是在新的值被賦值後才觸發,因此你無法透過取消Change Event的方式去來避免值的改變。

Event 描述 Trickles down Bubbles up Cancellable
ChangeEvent 當一個Element的值改變時,就會發送一個Change Event

專屬於Change Event的屬性

  • previousValue: 存放該Element先前的值 The previous value of the target control.
  • newValue: 存放該Element該要改變的新值

Change Event是一個通知事件(notification event)讓你可以在visual element的值改變時做出反應,例如:當使用者按下音樂核取方塊(Checkbox)那麼遊戲的音樂就應該都被關閉。

Change Event應用到所有有實作INotifyValueChanged<T>的controls其中<T>就是ChangeEvent,此外這個也被在內部使用,透過Data binding的方式來更新實體物件的屬性

避免觸發Change Event

如果你是透過程式碼更改control的值而觸發control的Change Event,你可以透過呼叫INotifyValueChange<T>中的SetValueWithoutNotify來更改control的值,並避免觸發Change Event

註冊Change Event
  1. 呼叫在Visual Element上的RegisterCallback<>()

    • 如果你想要在一個Element中監聽它的子Elements中是否有發生改變的話,可以使用
      1
      2
      3
      4
      5
      6
      7
      8
      // 使用RegisterCallback註冊callback
      rootVisualElement.RegisterCallback<ChangeEvent<bool>>(OnBoolChangedEvent);

      // callback方法,它會監聽bool值是否發生改變
      private void OnBoolChangedEvent(ChangeEvent<bool> evt)
      {
      // Handling code
      }
  2. 呼叫有實作INotifyValueChange的Visual Element上的RegisterValueChangedCallback()

    • 使用RegisterValueChangedCallback會比較方便因為它已經內建(build-in)好要傳值得類型
    • 取消註冊可以使用UnregisterValueChangedCallback
      1
      2
      3
      4
      5
      6
      var newToggle = new Toggle("Test Toggle");
      newToggle.RegisterValueChangedCallback(OnTestToggleChanged);
      private void OnTestToggleChanged(ChangeEvent<bool> evt)
      {
      // Handling code
      }
例子一:註冊兩個ChangeEvent,一個在toggle element上使用RegisterValueChangedCallback,另外一個在root element使用RegisterCallback

以下例子為示範如何使用ChangeEvent

  1. 在Assets > Scripts > Editor 下建立一個 C# Script ChangeEventTestWindow.cs
  2. 將以下程式碼複製到剛剛建立的C# Script中
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    using UnityEditor;
    using UnityEngine;
    using UnityEngine.UIElements;

    public class ChangeEventTestWindow : EditorWindow
    {
    private Toggle m_MyToggle;

    // 這個Attributes會在 Windows -> UI Tollkit 下加入一個 Change Event Test Window
    [MenuItem("Window/UI Toolkit/Change Event Test Window")]
    public static void ShowExample()
    {
    ChangeEventTestWindow wnd = GetWindow<ChangeEventTestWindow>();
    wnd.titleContent = new GUIContent("Change Event Test Window");
    }

    public void CreateGUI()
    {
    // 建立一個 toggle control
    m_MyToggle = new Toggle("Test Toggle") { name = "My Toggle" };
    rootVisualElement.Add(m_MyToggle);

    // 使用RegisterValueChangedCallback為這個 toggle control註冊OnTestToggleChanged
    m_MyToggle.RegisterValueChangedCallback(OnTestToggleChanged);

    // 使用RegisterCallback在root element註冊OnBoolChangedEvent,它必須要指定泛型
    rootVisualElement.RegisterCallback<ChangeEvent<bool>>(OnBoolChangedEvent);
    }

    private void OnBoolChangedEvent(ChangeEvent<bool> evt)
    {
    Debug.Log($"Toggle changed. Old value: {evt.previousValue}, new value: {evt.newValue}");
    }

    private void OnTestToggleChanged(ChangeEvent<bool> evt)
    {
    Debug.Log($"A bool value changed. Old value: {evt.previousValue}, new value: {evt.newValue}");
    }
    }
  3. 在Unity編輯器中找到 Window > UI Toolkit > Change Events Test Window
  4. 觀看結果
例子二:使用程式碼觸發事件的話,可以透過element上的SetValueWithoutNotify避免觸發Change Event
  • 將以下程式碼覆蓋到ChangeEventTestWindow.cs Script中
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    using UnityEditor;
    using UnityEngine;
    using UnityEngine.UIElements;

    public class ChangeEventTestWindow : EditorWindow
    {
    private Toggle m_MyToggle;

    [MenuItem("Window/UI Toolkit/Change Event Test Window")]
    public static void ShowExample()
    {
    GetWindow<ChangeEventTestWindow>().titleContent = new GUIContent("Change Event Test Window");
    }

    public void CreateGUI()
    {
    // 建立一個toggle並為其註冊callback
    m_MyToggle = new Toggle("Test Toggle") { name = "My Toggle" };
    m_MyToggle.RegisterValueChangedCallback((evt) => { Debug.Log("Change Event received"); });
    rootVisualElement.Add(m_MyToggle);

    // 建立一個button來更改toggle的值
    Button button01 = new Button() { text = "Toggle" };
    button01.clicked += () =>
    {
    // 這個會觸發Change Event
    m_MyToggle.value = !m_MyToggle.value;
    };
    rootVisualElement.Add(button01);

    // 建立另外一個button,但是使用SetValueWithoutNotify更改toggle的值,
    Button button02 = new Button() { text = "Toggle without notification" };
    button02.clicked += () =>
    {
    // 使用SetValueWithoutNotify()不會觸發Change Event
    m_MyToggle.SetValueWithoutNotify(!m_MyToggle.value);
    };
    rootVisualElement.Add(button02);
    }
    }
  • 觀看結果

Reference: https://docs.unity3d.com/Manual/UIE-Change-Events.html

事件捕捉(Capture events)

事件捕捉(Capture events)通知你滑鼠捕捉狀態(mouse capture state)的變化,在UI Toolkit中的事件有兩捕捉種類型:

  1. 滑鼠捕捉事件(Mouse capture events)
  2. 指標捕捉事件(Pointer capture events)

當一個element捕捉了滑鼠或指標,它是唯一接收來自點擊裝置(Pointing device,如滑鼠)事件的element,直到該裝置釋放或失去捕捉為止。例如:假設你使用滑鼠點擊了一個文字框(text box),這個文字框會捕捉這個滑鼠,此時滑鼠仍然可以移動,但是它不會觸發文字框之外的事件;只要這個文字框正在捕捉你的滑鼠,那麼它就不會觸發其他事件。當你在文字框之外按下滑鼠上的按鈕時,文字框會釋放其滑鼠捕捉。

捕捉滑鼠(Mouse capture)

滑鼠捕捉事件(Mouse capture events)是實體滑鼠或是模擬滑鼠的虛擬滑鼠上的事件。捕捉到滑鼠時,也會產生一個滑鼠指標的PointerCaptureEvent。當一個element釋放捕捉滑鼠時,會觸發相應的MouseCaptureOutEvent

注意:不會發生同時有兩個elements同時捕捉滑鼠的情況,如果另外一個Visual Element觸發了MouseCaptureEvent那麼原先捕捉滑鼠的Element就會釋滑鼠並收到一個MouseCaptureOutEvent

捕捉指標(Pointer capture)

在UI Toolkit中,指標事件優先於滑鼠事件。如果指標的類型是滑鼠的話,在捕捉到指標事件時也會觸發滑鼠事件。

Event 描述 Trickles down Bubbles up Cancellable
MouseCaptureEvent 當某個Element被滑鼠捕捉時會發送這個事件,此時target為這個Element
MouseCaptureOutEvent 當某個Element捕捉的滑鼠被釋放或是某些其他原因是放時,會發送這個事件,此時target為這個失去滑鼠捕捉的Element
PointerCaptureEvent 當某個Element捕捉指標時會發送這個事件,此時target為這個Element
PointerCaptureOutEvent 當某個Element捕捉的指標被釋放時,會發送這個事件,此時target為這個失去指標捕捉的Element
例子

以下例子為示範捕捉與釋放的行為

  1. Assets > Scripts > Editor 下建立一個 C# Script CaptureEventsTestWindow.cs

  2. 將以下程式碼複製到剛剛建立的C# Script中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    using UnityEditor;
    using UnityEngine;
    using UnityEngine.UIElements;

    public class CaptureEventsTestWindow : EditorWindow
    {
    // 這個Attributes會在 Windows -> UI Tollkit 下加入一個 Capture Events Test Window
    [MenuItem("Window/UI Toolkit/Capture Events Test Window")]
    public static void ShowExample()
    {
    var wnd = GetWindow<CaptureEventsTestWindow>();
    wnd.titleContent = new GUIContent("Capture Events Test Window");
    }

    private bool m_IsCapturing = false;

    public void CreateGUI()
    {
    for (int i = 0; i < 4; i++)
    {
    // 建立一個 Label 的Visual Element
    Label clickableLabel = new Label($"Label {i} - Click Me!");
    // 為這個Label Element註冊 MouseDownEvent
    clickableLabel.RegisterCallback<MouseDownEvent>((evt) => {
    // 這個匿名Callback會在Console中印出 Clicked on label *** 的字串
    Debug.Log($"Clicked on label '{(evt.target as Label).text}'");
    });
    rootVisualElement.Add(clickableLabel);
    }

    // 建立一個 Label 的Visual Element
    Label capturingLabel = new Label("Click here to capture mouse");
    // 為這個Label Element註冊 MouseDownEvent
    capturingLabel.RegisterCallback<MouseDownEvent>((evt) =>
    {
    if (!m_IsCapturing)
    {
    capturingLabel.text = "Click here to release mouse";
    // 捕捉滑鼠
    MouseCaptureController.CaptureMouse(capturingLabel);
    m_IsCapturing = true;
    }
    else
    {
    capturingLabel.text = "Click here to capture mouse";
    // 釋放捕捉
    MouseCaptureController.ReleaseMouse(capturingLabel);
    m_IsCapturing = false;
    }
    });
    rootVisualElement.Add(capturingLabel);

    // 註冊捕捉滑鼠事件
    rootVisualElement.RegisterCallback<MouseCaptureEvent>((evt) =>
    {
    Debug.Log("Mouse captured");
    });

    // 註冊滑鼠釋放捕捉事件
    rootVisualElement.RegisterCallback<MouseCaptureOutEvent>((evt) =>
    {
    Debug.Log("Mouse captured released");
    });
    }
    }
  3. 在Unity編輯器中找到 Window > UI Toolkit > Capture Events Test Window

  4. 點擊Test Window上面的Label來觀看結果

Reference: https://docs.unity3d.com/Manual/UIE-Change-Events.html

合成並發送事件(Synthesize and send events)

事件系統使用一個事件池(pool of events),以避免重複分配事件物件(event object)。要合成並發送自己的事件需要:

  1. 建立一個UnityEngine.Event用來持有初始化的資料
  2. 從事件池中獲取一個事件物件。
  3. 填寫事件的屬性(event properties)。
  4. 將事件封裝在using區塊中,以確保它被返回到事件池。
  5. 將事件傳遞給panel.visualTree.SendEvent()

下面的例子中會發送一個KeyDownEvent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void SynthesizeAndSendKeyDownEvent(IPanel panel, KeyCode code,
char character = '\0', EventModifiers modifiers = EventModifiers.None)
{
// 建立一個UnityEngine.Event用來持有初始化的資料
var evt = new Event() {
type = EventType.KeyDownEvent,
keyCode = code,
character = character,
modifiers = modifiers
};

// 將事件封裝在`using`區塊中,以確保它被返回到事件池
using (KeyDownEvent keyDownEvent = KeyDownEvent.GetPooled(evt))
{
panel.visualTree.SendEvent(keyDownEvent);
}
}

注意:

  1. 不能發送不是來自OS(operating system)的事件,
  2. 不能發送不在UnityEngine.Event types中的事件
  3. 某些事件是由UI Toolkit作為對內部狀態變化的反應而發送的,不應由外部過程發送。例如,如果您發送PointerCaptureEvent,visual elements會假定該事件的底層條件已滿足,並且不會為它們設置指針捕獲(pointer capture)。這可能會破壞visual elements的內部配置(configurations)並導致未定義的行為。

Event reference

Reference: https://docs.unity3d.com/Manual/UIE-Events-Synthesizing.html

事務( Transaction )

事務(Transaction)

事務或是交易(Transaction)是指資料庫中邏輯上的一系列操作(operation),這組操作要就全部成功,要就全部不成功。

  • start transaction;或是begin;:為開啟事務,在這條SQL語句之後的SQL都將處於同一個事務中,這些SQL語句不會立刻執行。
  • commit;:為提交事務,一旦提交,事務中所有的SQL才會執行。
  • rollback;:回滾事務,回復在此事務內所有的動作。
  • 資料庫如果沒有自己去控制事務,預設是一條SQL就處在自己單獨的事務中。
事務的特性(ACID)

原子性(Atomicity):一個事務中的所有操作,要麼全部完成,要麼全部不完成,不會結束在中間某個環節。事務在執行過程中發生錯誤,會被回滾(Rollback)到事務開始前的狀態,就像這個事務從來沒有執行過一樣。

  • 例如:銀行帳戶存款和取款操作。每次存款或取款操作都應該是原子性的,即要麼全部成功,要麼全部失敗。
    • 用戶A要取款300元。在這個操作中,銀行應該首先檢查用戶A的帳戶餘額是否足夠,然後從帳戶餘額上減去取款金額。這個操作是原子性的,要麼成功從用戶A的帳戶中扣除300元,要麼失敗,不會出現只減少部分金額的情況。

一致性(Consistency)在事務開始之前和事務結束以後,資料庫的完整性沒有被破壞。這表示寫入的資料必須完全符合所有的預設規則,這包含資料的精確度、串聯性以及後續資料庫可以自發性地完成預定的工作。簡單來說就是在事務前後,資料必須保持一致,但在過程中不一定會保持一致性

  • 例如:A將1000元轉帳至B的帳戶,此事務包含兩個步驟,
    1. 銀行從A的帳戶中扣除1000元。
    2. 銀行將1000元存入B的帳戶中。
    • 在事務前的A與B的帳戶金額總和一定會和事務後的A與B帳戶內的金額總相同 => 這就是事務前後,資料必須保持一致
    • 但是在各操作時,資料不一致,例如銀行從A的帳戶中扣除1000元這一個操作做完之後,銀行資料庫的金額會處於不一致狀態(少了1000元),等到第二操作(銀行將1000元存入B的帳戶)之後資料庫才會保持一致性的狀態。

隔離性(Isolation):資料庫允許多個併發事務同時對其資料進行讀寫和修改的能力,隔離性可以防止多個事務併發執行時由於交叉執行而導致資料數據的不一致。事務隔離分為不同級別,包括

  • 讀未提交(Read uncommitted):允許讀取其他事務尚未提交的資料。因此,可能發生髒讀不可重複讀幻讀
  • 讀提交(Read committed):只能讀取已經提交的資料。因此,可能發生不可重複讀幻讀,但不會發生髒讀
  • 可重複讀(Repeatable read):在同一個事務中多次讀取相同資料時,保證資料的一致性。因此,可能發生幻讀,但不會發生髒讀不可重複讀
  • 串行化(Serializable):最高的隔離級別,確保並發事務之間的串行執行,避免任何並發問題。因此,不會發生髒讀不可重複讀幻讀

持久性(Durability):事務處理結束後,對資料的修改就是永久的,即便系統故障也不會丟失。

事務隔離性導致的問題:

髒讀:髒讀發生在一個事務允許讀取另一個事務修改但未提交的資料時。

  • 例如,當一個事務A正在讀取資料並且對資料進行了修改,但這個修改還沒有被提交到資料庫中。此時,另一個事務B可能會讀取到這個未提交的修改,這就是髒讀的情況。

不可重複讀:在一次事務中,當一行資料獲取兩遍得到不同的結果表示發生了「不可重複讀」。例如,在一個事務A中多次讀取同一筆資料,在事務A還未結束時,另一個事務B也訪問了這個資料,並且修改這個資料並且commit,那麼事務A在多次讀取這筆資料時可能會讀到不同的資料。

  • 不可重複讀的重點是修改:同樣的條件,你讀取過的資料,再次讀取出來發現值不一樣了
  • 例如:
    1. 在交易A中,使用SELECT語句讀取賬戶餘額為1000元,然後執行其他操作。
    2. 在交易B中,進行了一次轉賬操作,將賬戶餘額從1000元減少到900元。
    3. 再次在交易A中,使用相同的SELECT語句讀取賬戶餘額,發現餘額為900元,與之前的餘額不一致。
    • 這是因為在兩次讀取之間,資料表中的資料已經被修改

幻讀:在事務執行過程中,當兩個完全相同的查詢語句執行得到不同的結果集。這種現象稱為「幻讀(phantom read)」。

  • 幻讀的重點在於新增或者刪除:同樣的條件,第1次和第2次讀出來的記錄數目不一幻讀
  • 例如:目前工資為1000的員工有10人。
    • 使用 SELECT count(*) FROM employee WHERE salary=1000; SQL,讀取所有工資為1000的員工。
    1. 事務A,讀取所有工資為1000的員工,共讀取到了10條記錄
    2. 這時事務B向employee表插入了一條員工記錄,工資也為1000
    3. 事務A,使用相同的SQL再次讀取所有工資為1000的員工,此時共讀取到了11條記錄
隔離級別 髒讀 不可重複讀 幻影讀
未提交讀 可能發生 可能發生 可能發生
提交讀 - 可能發生 可能發生
可重複讀 - - 可能發生
可序列化 - - -

一般情況下,大多數資料庫系統的預設隔離級別是可重複讀(Repeatable Read)。這意味著在同一個事務中,多次讀取相同的資料時,保證資料的一致性,並且防止不可重複讀取的情況發生。

使用grep在多個檔案中找出含有某個字串的檔案

有時候會想在很多個檔案中找出是否含有某個字串,這時可以使用grep


會找 /etc/ 目錄下檔案內容含有 like 的檔案,例如:likeliked

1
grep -nr "like" /etc/

會找 /etc/目錄下檔案內容含有 like 單字 的檔案,與上面的差異為加上-w之後,只匹配單詞like,而不是像liked這樣的子字串。

1
grep -rnw '/like' /etc/

加上-i之後,會忽略大小寫。可以匹配到LIKELiked,或是like…等

1
grep -rni '/like' /etc/

以下是常用的參數

1
2
3
4
5
-n 會印出關鍵字在檔案的行數
-r 會遞迴的找子資料夾
-w 只匹配一個單字
-y 忽略大小寫
-i 忽略大小寫

JavaScript 事件傳遞(Event Bubbling)

HTML結構

HTML DOM elements是嵌套(nested)的結構,如下:

1
2
3
4
5
<div onclick="alert('點擊最外層');">First              // 最外層
<div onclick="alert('點擊第二層');">Second
<div onclick="alert('點擊最內層');">Third</div> // 最內層
</div>
</div>

Event Bubbling

使用JavaScript可以對HTML DOM element註冊(register)一些不同的event handlers,Event bubbling是HTML DOM API中事件傳播(event propagation)的一種方式。

  • Bubbling事件傳播的方式為:從事件發生的地方開始一層一層的向最外層傳遞

下面的例子(可以用滑鼠點點看):

最外層
第二層
最內層

1
2
3
4
5
6
7
8
9
10
11
<div onclick="alert('點擊最外層');" style="background: red; border: 3px dotted black; text-align: center; width: 400px;"> 
最外層
<br />
<div onclick="alert('點擊第二層');" style="background: green; border: 4px dotted black; text-align: center; width:300px;">
第二層
<br />
<div onclick="alert('點擊最內層');" style="background: blue; border: 5px dotted black; text-align: center; width: 250px;">
<font color="white">最內層</font>
</div>
</div>
</div>

當你點下(也就是觸發onclick事件)最裡面藍色的那一層的時候,會彈出點擊最內層的字串,然後再彈出點擊第二層的字串,最後再彈出點擊最外層的字串。這就是Bubbling事件傳播

停止Event Bubbling

有時候會想要停止bubbling傳遞事件,可以透過以下的方式來停止,

  • event.stopPropagation();
  • event.cancelBubble = true; // IE<9

例如下面的例子(可以用滑鼠點點看):

最外層
第二層
最內層
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
function disableBubbling(){
alert('點擊了' + event.srcElement.className);
event.stopPropagation();
}
</script>
<div class="最外層" onclick="disableBubbling();" style="background: red; border: 3px dotted black; text-align: center; width: 400px;">
最外層<br />
<div class="第二層" onclick="disableBubbling();" style="background: green; border: 4px dotted black; text-align: center; width: 300px;">
第二層<br />
<div class="最內層" onclick="disableBubbling();" style="background: blue; border: 5px dotted black; text-align: center; width: 250px;">
<font color="white">最內層</font>
</div>
</div>
</div>

Manipulators

Manipulators是處理使用者與UI Element交互的State machine,它被用來儲存,註冊,取消註冊event callbacks

建立與使用Manipulators

你不需要自己撰寫管理callback的class,你只需要要繼承UI Toolkit提供的manipulators class就可以使用它們來管理callback與event,步驟如下:

  1. 建立一個class,這個class封裝了針對特定使用者互動所需的事件處理邏輯,並繼承了UI Toolkit提供的manipulators class。
  2. 在這個class中,實作方法(method)來回應相關的互動,例如滑鼠點擊或拖曳。這些方法捕獲並處理必要的資訊以執行這個互動行為(behavior)。
  3. 當你完成設計這個class之後,你就可以實體化它並將其附加到目標(target) UI Element上。這個附加使這個Manipulator class可以攔截和管理指定的事件,並協調使用者互動,同時與你的UI程式碼保持清晰的分離。
  4. 在Visual Element上使用AddManipulator將建立的Manipulator加入到這個Element
  5. 在Visual Element上使用RemoveManipulator為這個Element移除指定的的Manipulator

以下是UI Toolkit提供的Manipulator class
Manipulator 繼承自 描述
Manipulator 所有manipulators的基類(Base class)
KeyboardNavigationManipulator Manipulator 將特定設備輸入事件(device-specific input event)轉換為可以使用鍵盤進行的較高階導航操作(higher-level navigation operations)
MouseManipulator Manipulator 處理滑鼠輸入,擁有一個啟用過濾器(ManipulatorActivationFilter)列表
ContextualMenuManipulator MouseManipulator 當使用者按下滑鼠右鍵或是menu鍵時,顯示內容選單(contextual menu)
PointerManipulator MouseManipulator 處理指標(pointer)輸入,擁有一個啟用過濾器(ManipulatorActivationFilter)列表
Clickable PointerManipulator 追蹤element上的滑鼠事件並辨認是否發生點擊事件,也就是說在同一個element中是否有發生指標按下放開

例子

以下範例將示範

  • 如何一個繼承PointerManipulator,來處理滑鼠輸入,
  • 並使用activators list屬性來設定可以啟用這個manipulator的條件,
    • 例如:當使用者點擊滑鼠左鍵時,啟用這個manipulator
      • 要做到這個,你只要實體化一個ManipulatorActivationFilter,將其button屬性設為MouseButton.LeftMouse並加入到activators list即可
  • 使用target屬性來存取附加這個manipulator的element,
  • Override RegisterCallbacksOnTargetUnregisterCallbacksFromTarget方法用以註冊和取消註冊event callbacks。
建立一個可以讓你拖動element的manipulator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
using UnityEngine;
using UnityEngine.UIElements;

// 繼承PointerManipulator
public class ExampleDragger : PointerManipulator
{
private Vector3 m_Start;
protected bool m_Active;
private int m_PointerId;
private Vector2 m_StartSize;

public ExampleDragger()
{
m_PointerId = -1;
// 實體化一個`ManipulatorActivationFilter`,將其button屬性設為`MouseButton.LeftMouse`並加入到`activators` list
activators.Add(new ManipulatorActivationFilter { button = MouseButton.LeftMouse });
m_Active = false;
}

// Override RegisterCallbacksOnTarget方法用以註冊event callbacks
protected override void RegisterCallbacksOnTarget()
{
target.RegisterCallback<PointerDownEvent>(OnPointerDown);
target.RegisterCallback<PointerMoveEvent>(OnPointerMove);
target.RegisterCallback<PointerUpEvent>(OnPointerUp);
}

// Override UnregisterCallbacksFromTarget方法用以取消註冊event callbacks
protected override void UnregisterCallbacksFromTarget()
{
target.UnregisterCallback<PointerDownEvent>(OnPointerDown);
target.UnregisterCallback<PointerMoveEvent>(OnPointerMove);
target.UnregisterCallback<PointerUpEvent>(OnPointerUp);
}

protected void OnPointerDown(PointerDownEvent e)
{
if (m_Active)
{
e.StopImmediatePropagation();
return;
}

if (CanStartManipulation(e))
{
m_Start = e.localPosition;
m_PointerId = e.pointerId;

m_Active = true;
target.CapturePointer(m_PointerId);
e.StopPropagation();
}
}

protected void OnPointerMove(PointerMoveEvent e)
{
if (!m_Active || !target.HasPointerCapture(m_PointerId))
return;

Vector2 diff = e.localPosition - m_Start;

target.style.top = target.layout.y + diff.y;
target.style.left = target.layout.x + diff.x;

e.StopPropagation();
}

protected void OnPointerUp(PointerUpEvent e)
{
if (!m_Active || !target.HasPointerCapture(m_PointerId) || !CanStopManipulation(e))
return;

m_Active = false;
target.ReleaseMouse();
e.StopPropagation();
}
}
建立一個可以當拖動時,改變element大小的manipulator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
using UnityEngine;
using UnityEngine.UIElements;

public class ExampleResizer : PointerManipulator
{
private Vector3 m_Start;
protected bool m_Active;
private int m_PointerId;
private Vector2 m_StartSize;
public ExampleResizer()
{
m_PointerId = -1;
activators.Add(new ManipulatorActivationFilter { button = MouseButton.LeftMouse });
m_Active = false;
}

protected override void RegisterCallbacksOnTarget()
{
target.RegisterCallback<PointerDownEvent>(OnPointerDown);
target.RegisterCallback<PointerMoveEvent>(OnPointerMove);
target.RegisterCallback<PointerUpEvent>(OnPointerUp);
}

protected override void UnregisterCallbacksFromTarget()
{
target.UnregisterCallback<PointerDownEvent>(OnPointerDown);
target.UnregisterCallback<PointerMoveEvent>(OnPointerMove);
target.UnregisterCallback<PointerUpEvent>(OnPointerUp);
}

protected void OnPointerDown(PointerDownEvent e)
{
if (m_Active)
{
e.StopImmediatePropagation();
return;
}

if (CanStartManipulation(e))
{
m_Start = e.localPosition;
m_StartSize = target.layout.size;
m_PointerId = e.pointerId;
m_Active = true;
target.CapturePointer(m_PointerId);
e.StopPropagation();
}
}

protected void OnPointerMove(PointerMoveEvent e)
{
if (!m_Active || !target.HasPointerCapture(m_PointerId))
return;

Vector2 diff = e.localPosition - m_Start;

target.style.height = m_StartSize.y + diff.y;
target.style.width = m_StartSize.x + diff.x;

e.StopPropagation();
}

protected void OnPointerUp(PointerUpEvent e)
{
if (!m_Active || !target.HasPointerCapture(m_PointerId) || !CanStopManipulation(e))
return;

m_Active = false;
target.ReleasePointer(m_PointerId);
m_PointerId = -1;
e.StopPropagation();
}
}
加入manipulator
1
2
3
4
var myElement = new VisualElement();

// 加入manipulator到這個VisualElement
myElement.AddManipulator(new ExampleDragger());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var box = new VisualElement()
{
style =
{
left = 100,
top = 100,
width = 100,
height = 100,
backgroundColor = Color.red
},
pickingMode = PickingMode.Position,
};

box.AddManipulator(new ExampleResizer());
移除manipulator
1
2
// 為這個VisualElement移除manipulator
myElement.RemoveManipulator<ExampleDragger>();

處理事件(Handle Event)

UI Toolkit Events類似於HTML Events,當事件(Event)發生時,該事件就會發送給target visual element以及在事件傳播路徑(propagation path)上的visual elements。

處理事件的順序如下:

  1. 由root element往下直到目標(target)的parent element,都會執行event callback,稱為trickle-down phase
  2. 在事件目標執行event callback,稱為target phase
  3. 在事件目標上面呼叫ExecuteDefaultActionAtTarget()
  4. 由事件目標的parent element往上直到root element都會執行event callbacks,稱為bubble-up phase
  5. 在事件目標上面呼叫ExecuteDefaultAction()

註冊Event Callbacks(Register an event callback)

可以在class中註冊callback,為個別的instance自訂行為(behavior),例如滑鼠點擊text label時作出反應。
除了目標(target)以外,沿著傳播路徑經過的element都會接收到兩次event

  1. 一次是在trickle-down phase
  2. 另外一次是bubble-up phase

使用element的RegisterCallback方法來註冊一個event callback

  • element.RegisterCallback<MouseDownEvent>(CallbackMethod); 此例中把CallbackMethod註冊到MouseDownEvent
1
myElement.RegisterCallback<MouseDownEvent>(MyCallback);

預設中,註冊的event callback會在target phasebubble-up phase的時候執行,這樣可以確保父element會在子element之後做出反應。

  • 如果你想要父element在子element之前做出反應可以在註冊event時,將其設定為TrickleDown.TrickleDown
1
2
3
4
5
6
7
8
9
10
11
12
13
using UnityEngine;
using UnityEngine.UIElements;

...
VisualElement myElement = new VisualElement();

// 為這個myElement註冊一個trickle-down phase的callback,
// 這會讓dispatcher在target phase與trickle-down phase時執行此MyCallback
myElement.RegisterCallback<MouseDownEvent>(MyCallback, TrickleDown.TrickleDown);

// 定義一個callback
void MyCallback(MouseDownEvent evt) { /* ... */ }
...

在一個element中,你可以對同一個event註冊多個callback,但是你只能在同一事件和傳播階段註冊一次相同的callback。

有參數的callback
1
2
3
4
5
// 此將會向MyCallbackWithData傳送一個MyType的參數
myElement.RegisterCallback<MouseDownEvent, MyType>(MyCallbackWithData, myData);

// 定義一個有參數的callback
void MyCallbackWithData(MouseDownEvent evt, MyType data) { /* ... */ }

移除註冊的callback

使用myElement.UnregisterCallback()移除註冊的callback。


監聽value

UI controls會使用value屬性來存放資料,例如

  • Toggle會持有一個boolean屬性,當Toggle關閉(off)時設為false,開啟(on)時設為true。
  • IntegerField會持有一個integer屬性,用來保存該field的值。
    • int val = myIntegerField.value;
  • 透過RegisterValueChangedCallback你可以註冊一個監聽ChangeEvent事件的callback
    • RegisterValueChangedCallbackRegisterCallback<ChangeEvent>的簡寫,它是透過INotifyValueChange interface來為VisualElement推測正確的Type T
1
2
3
4
5
// 為myIntegerField註冊一個監聽值改變的OnIntegerFieldChange callback
myIntegerField.RegisterValueChangedCallback(OnIntegerFieldChange);

// 定義一個callback
void OnIntegerFieldChange(ChangeEvent<int> evt) { /* ... */ }

更改Value有以下方式

  • 直接更改value屬性,這會觸發一個新的ChangeEvent
    • myControl.value = myNewValue;
  • 使用該element的SetValueWithoutNotify()更改,這不會觸發一個新的ChangeEvent
    • myControl.SetValueWithoutNotify(myNewValue);

獲取Pointer

當visual element獲取一個Pointer時,Unity會向這個visual element發送所有與這個Pointer相關的Events,而不管是否這個Pointer懸停(hovers over)在這個visual element上,舉例來說,假設你建立了一個control,他會接收drag events並獲取pointer,不論pointer的位置為何,這個control都會接收drag events


Event與自訂Control

當自訂Control時,有兩種方式回應UI Toolkit events

  1. 註冊一個event callback
  2. 實作一個default action

callbacks與default action的差異

  • callback必須要註冊在一個實體物件上;default action則是類似在class上的virtual functions
  • 所有在傳播路徑(propagation path)上的visual element都會執行callback;Default actions只會在event target上執行。
  • callbacks可能會需要額外檢查是否是需要做出反應的event;default actions可以省略這個步驟
  • callbacks效能可能較差,default actions會好一些
    • 因為callbacks在propagation phase時需要在註冊表(callback registry)裡面搜尋,而default actions則不需要
實作Default Action

若要實作Default Action的話,需要繼承VisualElement,並實作ExecuteDefaultActionAtTarget()或是ExecuteDefaultAction(),或兩個方法都實作。
如果實作了Default Action,會應用到該class的所有實體(instances)

  • 如果你想要在parent callback之前執行default action可以把default action放到ExecuteDefaultActionAtTarget()
  • 應該將default actions視為該element type要有的行為(behaviors),例如:chcekbox觸發click event更新它的狀態時應override default action,而不是為所有checkbox註冊一個callback
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
override void ExecuteDefaultActionAtTarget(EventBase evt)
{
// Call the base function.
base.ExecuteDefaultActionAtTarget(evt);

if (evt.eventTypeId == MouseDownEvent.TypeId())
{
// ...
}
else if (evt.eventTypeId == MouseUpEvent.TypeId())
{
// ...
}
// More event types
}

custom controls的最佳實踐

  1. 如果是element有的行為(behaviors),你應該實作在default action
  2. 在callback中,你可以使用PreventDefault()來取消該element的 default action
  3. 若一個instance沒有callback,那麼它不會進入propagation process
  4. 在事件分派過程(event dispatch process)中,有兩個時段可以執行你的default action
    • 若想在trickle-downbubble-up之間,在target callbacks之後立刻執行的話,override這個ExecuteDefaultActionsAtTarget()方法
    • 在事件分派過程(event dispatch process)結束時,override這個ExecuteDefaultActions()方法
  5. 如果可能的話,盡量將default action寫在ExecuteDefaultActions(),在事件傳播過程(event propagation process)中的trickle-down階段或是bubble-up階段,可以透過PreventDefault()來覆蓋預設的操作。
  6. 如果一個事件不應該傳播到parent element的話,你必須要把傳播停下
    • 例如:一個TextField在收到一個KeyDownEvent去修改它的值時,不應該讓這個事件傳播到它的parent visual element
    • 使用ExecuteDefaultActionsAtTarget()實作default action時,可以呼叫StopPropagation()這可以確保event在bubble-up階段時不會處理
  7. Default actions僅針對event target執行,要讓class對target的child element或parent element的事件作出反應,則必須在event的trickle-down或是bubble-up傳播階段之一註冊callback以接收該事件。
  8. 為了提高性能,盡量避免在的class中註冊回調函式。
  9. 一個parent panel可以在trickle-down階段停止傳播,避免事件傳到它的children
  10. 你無法在event class本身中阻止EventBase.PreDispatch()EventBase.PostDispatch()方法的執行
  11. 可以透過以下方法事件傳播(event propagation)
    • StopImmediatePropagation():立刻停止事件傳播,其他callback不會執行這個event,但是ExecuteDefaultActionAtTarget()ExecuteDefaultAction() default actions 仍然會執行。
    • StopPropagation():在這個element上的最後一個callback執行完之後,停止傳播。這可以確保所有在這個element上的callback都執行完,不會有更多element去對這個事件做出反應。ExecuteDefaultActionAtTarget()ExecuteDefaultAction() default actions 仍然會執行。
    • PreventDefault():在事件傳播過程(event propagation process)時,避免呼叫ExecuteDefaultActionAtTarget()ExecuteDefaultAction()
      • PreventDefault()不會阻止其他callback的執行。
      • 在bubble-up階段時,PreventDefault()不會影響ExecuteDefaultActionAtTarget() action。

Reference:https://docs.unity3d.com/Manual/UIE-Events-Handling.html