# 焦点与验证

> 焦点控制（focusOnTouch/focusOnKey）、可编辑性、限制以及提交时验证。

Source: https://crossfly.dev/zh/docs/ui/forms-and-input/focus-and-validation — CrossFly documentation. Full docs as one file: https://crossfly.dev/llms-full.txt

# 焦点与验证

由于**不存在按键事件**，CrossFly 中的验证在**提交时**（或失去焦点时）进行：你读取
`getText()`，做出判断，并标示错误。

## 完整示例（复制、粘贴并运行）
```java
package com.exemplo;

import dev.crossfly.ui.*;
import dev.crossfly.ui.layout.*;
import dev.crossfly.ui.style.*;   // TextFieldStyle（行为配置）

/** 可执行的主界面。将 launcher 指向此类（classname）并运行：gradlew :lwjgl3:run */
public class MainWindow extends Window
{
   @Override
   public void onShow() throws Exception
   {
      super.onShow();
      setLayout(new VerticalLayout());

      TextField nome  = new TextField().caption("Nome");
      add(LAY_AUTO, nome);

      TextField email = new TextField().caption("E-mail");
      add(LAY_AUTO, email);

      // 只读字段：显示值，点击时不弹出键盘也不聚焦
      TextField codigo = new TextField("ABC-123").caption("Código");
      codigo.<TextFieldStyle>getStyle().editable(false);
      add(LAY_AUTO, codigo);

      // 校验在提交时进行（没有按键事件）：
      // 空字段 → Hint.show(...) + 将焦点返回到出错字段。
      Button enviar = new Button("Cadastrar");
      enviar.onAction(c -> {
         String n = nome.getText().trim();
         String e = email.getText().trim();
         if (n.isEmpty())         { Hint.show("Informe o nome", 2000);  nome.setFocused(true);  return; }
         if (e.indexOf('@') < 0)  { Hint.show("E-mail inválido", 2000); email.setFocused(true); return; }
         Hint.show("Cadastrado: " + n, 2000);
      });
      add(LAY_AUTO, enviar);
   }
}
```

> 下面的片段**仅展示核心部分**（放入 `onShow()` 内的内容）。要运行它们，请放入上面的 `MainWindow` 骨架中。

## 焦点
| 属性（样式）/ 方法 | 效果 |
|---|---|
| `focusOnTouch`（样式） | 点击字段获得焦点（字段样式的默认行为） |
| `focusOnKey`（样式） | 在父级聚焦时按键将焦点移到字段 |
| `onFocus()` / `onBlur()` | 获得 / 失去焦点时调用的钩子（在子类中重写） |
| `hasFocus()` | 查询字段是否已聚焦 |
| `restoreFocusAtEnd()` | 将光标重新定位到末尾（通过选择器填写后） |

```java
// 只读字段（显示值，点击时不弹出键盘也不聚焦）：
saldo.<TextFieldStyle>getStyle().editable(false);
```

## 输入限制
```java
telefone.<TextFieldStyle>getStyle()
   .maxLength(11)                    // 最多 11 个字符
   .capitalise(Capitalise.UPPER);   // 强制大写（Capitalise: NORMAL/UPPER/LOWER）
```

## 提交时验证（推荐模式）
```java
Button enviar = new Button("Cadastrar");
enviar.onAction(c -> {
   String nome  = nomeField.getText().trim();
   String email = emailField.getText().trim();
   if (nome.isEmpty()) { Hint.show("Informe o nome", 2000); nomeField.setFocused(true); return; }
   if (email.indexOf('@') < 0) { Hint.show("E-mail inválido", 2000); emailField.setFocused(true); return; }
   salvar(nome, email);
});
```
- `Hint.show(...)` 用于非阻塞式提示；`Alert` 用于确认（参见
  [对话框与模态框](/docs/ui/navigation/dialogs-and-modals)）。
- 将焦点返回到出错的字段，以便用户更正。

## 最佳实践
- 通过读取 `getText()`/`getTextWithoutMask()` **在提交时**验证——不要试图拦截每次按键。
- 对于计算/派生字段使用 `editable(false)`，而不是"禁用并强制只读"。
- 在源头限制输入（`mask`、`maxLength`、`EditMode.NUMERIC`）以减少验证。

## 常见错误
- 寻找按键变化的监听器——它不存在；请在提交时/离开字段时验证。
- 忘记将焦点返回到无效字段，导致用户不知所措。
- 当规则是关于数字时验证 `getText()`（带掩码）→ 使用 `getTextWithoutMask()`。

## 另见
- [文本字段](/docs/ui/forms-and-input/text-fields) · [掩码与键盘](/docs/ui/forms-and-input/masks-and-keyboards) · [对话框与模态框](/docs/ui/navigation/dialogs-and-modals)
