Chapter 3.8: Dialogs & Modals
Home | << Previous: Styles, Fonts & Images | Dialogs & Modals | Next: Real Mod UI Patterns >>
模态 vs. 非模态
There are two fundamental types of dialog:
- Modal -- Blocks all interaction with content behind the dialog. The user must respond (confirm, cancel, close) before doing anything else. 示例: quit confirmation, delete warning, rename prompt.
- Modeless -- Allows the user to interact with content behind the dialog while it remains open. 示例: info panels, settings windows, tool palettes.
In DayZ, the distinction is controlled by whether you lock game input when the dialog opens. A modal dialog calls ChangeGameFocus(1) and shows the cursor; a modeless dialog may skip this or use a toggle approach.
UIScriptedMenu -- The Built-in System
UIScriptedMenu is 引擎-level 基类 for all menu screens in DayZ. It integrates with the UIManager menu stack, handles input locking automatically, and provides lifecycle hooks. Vanilla DayZ uses it for the in-game menu, logout dialog, respawn dialog, options menu, and many others.
类层次结构
UIMenuPanel (base: menu stack, Close(), submenu management)
UIScriptedMenu (scripted menus: Init(), OnShow(), OnHide(), Update())Minimal UIScriptedMenu Dialog
class MyDialog extends UIScriptedMenu
{
protected ButtonWidget m_BtnConfirm;
protected ButtonWidget m_BtnCancel;
protected TextWidget m_MessageText;
override Widget Init()
{
layoutRoot = GetGame().GetWorkspace().CreateWidgets(
"MyMod/GUI/layouts/my_dialog.layout");
m_BtnConfirm = ButtonWidget.Cast(
layoutRoot.FindAnyWidget("BtnConfirm"));
m_BtnCancel = ButtonWidget.Cast(
layoutRoot.FindAnyWidget("BtnCancel"));
m_MessageText = TextWidget.Cast(
layoutRoot.FindAnyWidget("MessageText"));
return layoutRoot;
}
override void OnShow()
{
super.OnShow();
// super.OnShow() calls LockControls() which handles:
// GetGame().GetInput().ChangeGameFocus(1);
// GetGame().GetUIManager().ShowUICursor(true);
}
override void OnHide()
{
super.OnHide();
// super.OnHide() calls UnlockControls() which handles:
// GetGame().GetInput().ChangeGameFocus(-1);
// GetGame().GetUIManager().ShowUICursor(false);
}
override bool OnClick(Widget w, int x, int y, int button)
{
super.OnClick(w, x, y, button);
if (w == m_BtnConfirm)
{
// Do action
Close();
return true;
}
if (w == m_BtnCancel)
{
Close();
return true;
}
return false;
}
override void Update(float timeslice)
{
super.Update(timeslice);
// ESC to close
if (GetUApi().GetInputByID(UAUIBack).LocalPress())
{
Close();
}
}
}打开和关闭
// Opening -- create the menu and push it onto the UIManager stack
MyDialog dialog = new MyDialog();
GetGame().GetUIManager().ShowScriptedMenu(dialog, null);
// Closing from outside
GetGame().GetUIManager().HideScriptedMenu(dialog);
// Closing from inside the dialog class
Close();ShowScriptedMenu() pushes the menu onto 引擎 menu stack, triggers Init(), then OnShow(). Close() triggers OnHide(), pops it from the stack, and destroys the widget tree.
关键生命周期方法
| Method | 调用时机 | 典型用途 |
|---|---|---|
Init() | Once, when menu is created | Create widgets, cache references |
OnShow() | After menu becomes visible | Lock input, start timers |
OnHide() | After menu is hidden | Unlock input, cancel timers |
Update(float timeslice) | Every frame while visible | Poll input (ESC key), animations |
Cleanup() | Before destruction | Release resources |
LockControls / UnlockControls
UIScriptedMenu provides built-in methods that OnShow() and OnHide() call automatically:
// Inside UIScriptedMenu (engine code, simplified):
void LockControls()
{
g_Game.GetInput().ChangeGameFocus(1, INPUT_DEVICE_MOUSE);
g_Game.GetUIManager().ShowUICursor(true);
g_Game.GetInput().ChangeGameFocus(1, INPUT_DEVICE_KEYBOARD);
g_Game.GetInput().ChangeGameFocus(1, INPUT_DEVICE_GAMEPAD);
}
void UnlockControls()
{
g_Game.GetInput().ChangeGameFocus(-1, INPUT_DEVICE_MOUSE);
g_Game.GetInput().ChangeGameFocus(-1, INPUT_DEVICE_KEYBOARD);
g_Game.GetInput().ChangeGameFocus(-1, INPUT_DEVICE_GAMEPAD);
// Cursor visibility depends on whether a parent menu exists
}Because UIScriptedMenu handles focus management automatically in OnShow()/OnHide(), you rarely need to call ChangeGameFocus() yourself when using this 基类. Simply call super.OnShow() and super.OnHide().
内置 ShowDialog(原生消息框)
The engine provides a native dialog system for simple confirmation prompts. It renders a platform-appropriate dialog box without requiring any layout file.
用法
// Show a Yes/No confirmation dialog
const int MY_DIALOG_ID = 500;
g_Game.GetUIManager().ShowDialog(
"Confirm Action", // caption
"Are you sure you want to do this?", // text
MY_DIALOG_ID, // custom ID to identify this dialog
DBT_YESNO, // button configuration
DBB_YES, // default button
DMT_QUESTION, // icon type
this // handler (receives OnModalResult)
);接收结果
The handler (the UIScriptedMenu passed as the last argument) receives the result through OnModalResult:
override bool OnModalResult(Widget w, int x, int y, int code, int result)
{
if (code == MY_DIALOG_ID)
{
if (result == DBB_YES)
{
PerformAction();
}
// DBB_NO means user declined -- do nothing
return true;
}
return false;
}常量
Button configurations (DBT_ -- DialogBoxType):
| 常量 | 显示按钮 |
|---|---|
DBT_OK | OK |
DBT_YESNO | Yes, No |
DBT_YESNOCANCEL | Yes, No, Cancel |
Button identifiers (DBB_ -- DialogBoxButton):
| 常量 | 值 | 含义 |
|---|---|---|
DBB_NONE | 0 | No default |
DBB_OK | 1 | OK button |
DBB_YES | 2 | Yes button |
DBB_NO | 3 | No button |
DBB_CANCEL | 4 | Cancel button |
Message types (DMT_ -- DialogMessageType):
| 常量 | 图标 |
|---|---|
DMT_NONE | No icon |
DMT_INFO | Info |
DMT_WARNING | Warning |
DMT_QUESTION | Question mark |
DMT_EXCLAMATION | Exclamation |
何时使用 ShowDialog
Use ShowDialog() for simple alerts and confirmations that do not need custom styling. It is reliable and handles focus/cursor automatically. For branded or complex dialogs (custom layout, input fields, multiple options), build your own dialog class.
手动对话框模式(无 UIScriptedMenu)
When you need a dialog that is not part of 引擎 menu stack -- 例如, a popup inside an existing panel -- extend ScriptedWidgetEventHandler instead of UIScriptedMenu. This gives you full control but requires manual focus and lifecycle management.
基本模式
class SimplePopup : ScriptedWidgetEventHandler
{
protected Widget m_Root;
protected ButtonWidget m_BtnOk;
protected ButtonWidget m_BtnCancel;
protected TextWidget m_Message;
void Show(string message)
{
m_Root = GetGame().GetWorkspace().CreateWidgets(
"MyMod/GUI/layouts/simple_popup.layout");
m_Root.SetHandler(this);
m_BtnOk = ButtonWidget.Cast(m_Root.FindAnyWidget("BtnOk"));
m_BtnCancel = ButtonWidget.Cast(m_Root.FindAnyWidget("BtnCancel"));
m_Message = TextWidget.Cast(m_Root.FindAnyWidget("Message"));
m_Message.SetText(message);
// Lock game input so player cannot move/shoot
GetGame().GetInput().ChangeGameFocus(1);
GetGame().GetUIManager().ShowUICursor(true);
}
void Hide()
{
if (m_Root)
{
m_Root.Unlink();
m_Root = null;
}
// Restore game input -- MUST match the +1 from Show()
GetGame().GetInput().ChangeGameFocus(-1);
GetGame().GetUIManager().ShowUICursor(false);
}
void ~SimplePopup()
{
Hide();
}
override bool OnClick(Widget w, int x, int y, int button)
{
if (w == m_BtnOk)
{
OnConfirm();
Hide();
return true;
}
if (w == m_BtnCancel)
{
Hide();
return true;
}
return false;
}
protected void OnConfirm()
{
// Override in subclasses or set a callback
}
}VPP-Style Popup (OnWidgetScriptInit Pattern)
VPP Admin Tools and other mods use OnWidgetScriptInit() to initialize popups. The widget is created by a parent, and the script class is attached via scriptclass in the layout file:
class MyPopup : ScriptedWidgetEventHandler
{
protected Widget m_Root;
protected ButtonWidget m_BtnClose;
protected ButtonWidget m_BtnSave;
protected EditBoxWidget m_NameInput;
void OnWidgetScriptInit(Widget w)
{
m_Root = w;
m_Root.SetHandler(this);
m_BtnClose = ButtonWidget.Cast(m_Root.FindAnyWidget("BtnClose"));
m_BtnSave = ButtonWidget.Cast(m_Root.FindAnyWidget("BtnSave"));
m_NameInput = EditBoxWidget.Cast(m_Root.FindAnyWidget("NameInput"));
// Push dialog above other widgets
m_Root.SetSort(1024, true);
}
void ~MyPopup()
{
if (m_Root)
m_Root.Unlink();
}
override bool OnClick(Widget w, int x, int y, int button)
{
if (w == m_BtnClose)
{
delete this;
return true;
}
if (w == m_BtnSave)
{
string name = m_NameInput.GetText();
if (name != "")
{
SaveName(name);
delete this;
}
return true;
}
return false;
}
protected void SaveName(string name)
{
// Process the input
}
}The parent creates the popup by creating the layout widget as a child:
Widget popup = GetGame().GetWorkspace().CreateWidgets(
"MyMod/GUI/layouts/popup.layout", parentWidget);The engine automatically calls OnWidgetScriptInit() on the script class specified in the layout's scriptclass attribute.
对话框布局结构
A dialog layout typically has three layers: a full-screen root for click interception, a semi-transparent overlay for dimming, and the centered dialog panel.
布局文件示例
FrameWidget "DialogRoot" {
size 1 1 0 0 // Full screen
halign fill
valign fill
// Semi-transparent background overlay
ImageWidget "Overlay" {
size 1 1 0 0
halign fill
valign fill
color "0 0 0 180"
}
// Centered dialog panel
FrameWidget "DialogPanel" {
halign center
valign center
hexactsize 1
vexactsize 1
hexactpos 1
vexactpos 1
size 0 0 500 300 // 500x300 pixel dialog
// Title bar
TextWidget "TitleText" {
halign fill
size 1 0 0 30
text "Dialog Title"
font "gui/fonts/MetronBook24"
}
// Content area
MultilineTextWidget "ContentText" {
position 0 0 0 35
size 1 0 0 200
halign fill
}
// Button row at bottom
FrameWidget "ButtonRow" {
valign bottom
halign fill
size 1 0 0 40
ButtonWidget "BtnConfirm" {
halign left
size 0 0 120 35
text "Confirm"
}
ButtonWidget "BtnCancel" {
halign right
size 0 0 120 35
text "Cancel"
}
}
}
}关键布局原则
- Full-screen root -- The outermost widget covers the entire screen so clicks outside the dialog are intercepted.
- Semi-transparent overlay -- An
ImageWidgetor panel with alpha (e.g.,color "0 0 0 180") dims the background, visually indicating a modal state. - Centered panel -- Use
halign centerandvalign centerwith exact pixel sizes for predictable dimensions. - Button alignment -- Place buttons in a horizontal container at the bottom of the dialog panel.
确认对话框模式
A reusable confirmation dialog accepts a title, message, and callback. This is the most common dialog pattern 在 DayZ Mod 中.
实现
class ConfirmDialog : ScriptedWidgetEventHandler
{
protected Widget m_Root;
protected TextWidget m_TitleText;
protected MultilineTextWidget m_ContentText;
protected ButtonWidget m_BtnYes;
protected ButtonWidget m_BtnNo;
protected Class m_CallbackTarget;
protected string m_CallbackFunc;
void ConfirmDialog(string title, string message,
Class callbackTarget, string callbackFunc)
{
m_CallbackTarget = callbackTarget;
m_CallbackFunc = callbackFunc;
m_Root = GetGame().GetWorkspace().CreateWidgets(
"MyMod/GUI/layouts/confirm_dialog.layout");
m_Root.SetHandler(this);
m_TitleText = TextWidget.Cast(
m_Root.FindAnyWidget("TitleText"));
m_ContentText = MultilineTextWidget.Cast(
m_Root.FindAnyWidget("ContentText"));
m_BtnYes = ButtonWidget.Cast(
m_Root.FindAnyWidget("BtnYes"));
m_BtnNo = ButtonWidget.Cast(
m_Root.FindAnyWidget("BtnNo"));
m_TitleText.SetText(title);
m_ContentText.SetText(message);
// Ensure dialog renders above other UI
m_Root.SetSort(1024, true);
GetGame().GetInput().ChangeGameFocus(1);
GetGame().GetUIManager().ShowUICursor(true);
}
void ~ConfirmDialog()
{
if (m_Root)
m_Root.Unlink();
}
protected void SendResult(bool confirmed)
{
GetGame().GetInput().ChangeGameFocus(-1);
GetGame().GetUIManager().ShowUICursor(false);
// Call the callback function on the target object
GetGame().GameScript.CallFunction(
m_CallbackTarget, m_CallbackFunc, null, confirmed);
// Clean up -- defer deletion to avoid issues
GetGame().GetCallQueue(CALL_CATEGORY_GUI).CallLater(
DestroyDialog, 0, false);
}
protected void DestroyDialog()
{
delete this;
}
override bool OnClick(Widget w, int x, int y, int button)
{
if (w == m_BtnYes)
{
SendResult(true);
return true;
}
if (w == m_BtnNo)
{
SendResult(false);
return true;
}
return false;
}
}用法
// In the calling class:
void AskDeleteItem()
{
new ConfirmDialog(
"Delete Item",
"Are you sure you want to delete this item?",
this,
"OnDeleteConfirmed"
);
}
void OnDeleteConfirmed(bool confirmed)
{
if (confirmed)
{
DeleteSelectedItem();
}
}The callback uses GameScript.CallFunction() which invokes a function by name on the target object. 这是标准方式 DayZ mods implement dialog callbacks since Enforce Script does not support closures or delegates.
输入对话框模式
An input dialog adds an EditBoxWidget for text entry with validation.
class InputDialog : ScriptedWidgetEventHandler
{
protected Widget m_Root;
protected TextWidget m_TitleText;
protected EditBoxWidget m_InputBox;
protected ButtonWidget m_BtnOk;
protected ButtonWidget m_BtnCancel;
protected TextWidget m_ErrorText;
protected Class m_CallbackTarget;
protected string m_CallbackFunc;
void InputDialog(string title, string defaultText,
Class callbackTarget, string callbackFunc)
{
m_CallbackTarget = callbackTarget;
m_CallbackFunc = callbackFunc;
m_Root = GetGame().GetWorkspace().CreateWidgets(
"MyMod/GUI/layouts/input_dialog.layout");
m_Root.SetHandler(this);
m_TitleText = TextWidget.Cast(
m_Root.FindAnyWidget("TitleText"));
m_InputBox = EditBoxWidget.Cast(
m_Root.FindAnyWidget("InputBox"));
m_BtnOk = ButtonWidget.Cast(
m_Root.FindAnyWidget("BtnOk"));
m_BtnCancel = ButtonWidget.Cast(
m_Root.FindAnyWidget("BtnCancel"));
m_ErrorText = TextWidget.Cast(
m_Root.FindAnyWidget("ErrorText"));
m_TitleText.SetText(title);
m_InputBox.SetText(defaultText);
m_ErrorText.Show(false);
m_Root.SetSort(1024, true);
GetGame().GetInput().ChangeGameFocus(1);
GetGame().GetUIManager().ShowUICursor(true);
}
void ~InputDialog()
{
if (m_Root)
m_Root.Unlink();
}
override bool OnClick(Widget w, int x, int y, int button)
{
if (w == m_BtnOk)
{
string text = m_InputBox.GetText();
text.Trim();
if (text == "")
{
m_ErrorText.SetText("Name cannot be empty");
m_ErrorText.Show(true);
return true;
}
GetGame().GetInput().ChangeGameFocus(-1);
GetGame().GetUIManager().ShowUICursor(false);
// Send result as Param2: OK status + text
GetGame().GameScript.CallFunctionParams(
m_CallbackTarget, m_CallbackFunc, null,
new Param2<bool, string>(true, text));
GetGame().GetCallQueue(CALL_CATEGORY_GUI).CallLater(
DeleteSelf, 0, false);
return true;
}
if (w == m_BtnCancel)
{
GetGame().GetInput().ChangeGameFocus(-1);
GetGame().GetUIManager().ShowUICursor(false);
GetGame().GameScript.CallFunctionParams(
m_CallbackTarget, m_CallbackFunc, null,
new Param2<bool, string>(false, ""));
GetGame().GetCallQueue(CALL_CATEGORY_GUI).CallLater(
DeleteSelf, 0, false);
return true;
}
return false;
}
override bool OnChange(Widget w, int x, int y, bool finished)
{
if (w == m_InputBox)
{
// Hide error when user starts typing
m_ErrorText.Show(false);
// Submit on Enter key
if (finished)
{
OnClick(m_BtnOk, 0, 0, 0);
}
return true;
}
return false;
}
protected void DeleteSelf()
{
delete this;
}
}焦点管理
Focus management is the most critical aspect of dialog implementation. DayZ uses a reference-counted focus system -- every ChangeGameFocus(1) must be balanced by a ChangeGameFocus(-1).
工作原理
// Increment focus counter -- game input is suppressed while counter > 0
GetGame().GetInput().ChangeGameFocus(1);
// Show the mouse cursor
GetGame().GetUIManager().ShowUICursor(true);
// ... dialog interaction ...
// Decrement focus counter -- game input resumes when counter reaches 0
GetGame().GetInput().ChangeGameFocus(-1);
// Hide cursor (only if no other menus need it)
GetGame().GetUIManager().ShowUICursor(false);规则
Every +1 must have a matching -1. If you call
ChangeGameFocus(1)inShow(), you must callChangeGameFocus(-1)inHide(), with no exceptions.Call -1 even on error paths. If the dialog is destroyed unexpectedly (player dies, server disconnect), the destructor must still decrement. Put cleanup in the destructor as a safety net.
UIScriptedMenu handles this automatically. If you extend
UIScriptedMenuand callsuper.OnShow()/super.OnHide(), focus is managed for you. Only manage it manually when usingScriptedWidgetEventHandler.Per-device focus is optional. The engine supports per-device focus locking (
INPUT_DEVICE_MOUSE,INPUT_DEVICE_KEYBOARD,INPUT_DEVICE_GAMEPAD). For most mod dialogs, a singleChangeGameFocus(1)(no device argument) locks all input.ResetGameFocus() is a nuclear option. It forces the counter to zero. Use it only in top-level cleanup (e.g., when closing an entire admin tool), never inside individual dialog classes.
常见问题
| 错误 | 症状 |
|---|---|
Forgot ChangeGameFocus(-1) on close | Player cannot move, shoot, or interact after dialog closes |
Called -1 twice | Focus counter goes negative; next menu that opens will not properly lock input |
Forgot ShowUICursor(false) | Mouse cursor stays visible permanently |
Called ShowUICursor(false) when parent menu is still open | Cursor disappears while parent menu is still active |
Z 轴顺序和分层
When a dialog opens on top of existing UI, it must render above everything else. DayZ provides two mechanisms:
控件排序顺序
// Push widget above all siblings (sort value 1024)
m_Root.SetSort(1024, true);The SetSort() method sets the rendering priority. Higher values render on top. The second parameter (true) applies recursively to children. VPP Admin Tools use SetSort(1024, true) for all dialog boxes.
Layout Priority (Static)
In layout files, you can set priority directly:
FrameWidget "DialogRoot" {
// Higher values render on top
// Normal UI: 0-100
// Overlay: 998
// Dialog: 999
}最佳实践
- Overlay background: Use a high sort value (e.g., 998) for the semi-transparent background.
- Dialog panel: Use a higher sort value (e.g., 999 or 1024) for the dialog itself.
- Stacking dialogs: If your system supports nested dialogs, increment the sort value for each new dialog layer.
常见模式
Toggle Panel (Open/Close with Same Key)
class TogglePanel : ScriptedWidgetEventHandler
{
protected Widget m_Root;
protected bool m_IsVisible;
void Toggle()
{
if (m_IsVisible)
Hide();
else
Show();
}
protected void Show()
{
if (!m_Root)
{
m_Root = GetGame().GetWorkspace().CreateWidgets(
"MyMod/GUI/layouts/toggle_panel.layout");
m_Root.SetHandler(this);
}
m_Root.Show(true);
m_IsVisible = true;
GetGame().GetInput().ChangeGameFocus(1);
GetGame().GetUIManager().ShowUICursor(true);
}
protected void Hide()
{
if (m_Root)
m_Root.Show(false);
m_IsVisible = false;
GetGame().GetInput().ChangeGameFocus(-1);
GetGame().GetUIManager().ShowUICursor(false);
}
}ESC to Close
// Inside Update() of a UIScriptedMenu:
override void Update(float timeslice)
{
super.Update(timeslice);
if (GetUApi().GetInputByID(UAUIBack).LocalPress())
{
Close();
}
}
// Inside a ScriptedWidgetEventHandler (no Update loop):
// You must poll from an external update source, or use OnKeyDown:
override bool OnKeyDown(Widget w, int x, int y, int key)
{
if (key == KeyCode.KC_ESCAPE)
{
Hide();
return true;
}
return false;
}Click Outside to Close
Make the full-screen overlay widget clickable. When clicked, close the dialog:
class OverlayDialog : ScriptedWidgetEventHandler
{
protected Widget m_Root;
protected Widget m_Overlay;
protected Widget m_Panel;
void Show()
{
m_Root = GetGame().GetWorkspace().CreateWidgets(
"MyMod/GUI/layouts/overlay_dialog.layout");
m_Overlay = m_Root.FindAnyWidget("Overlay");
m_Panel = m_Root.FindAnyWidget("DialogPanel");
// Register handler on both overlay and panel widgets
m_Root.SetHandler(this);
}
override bool OnClick(Widget w, int x, int y, int button)
{
// If user clicked the overlay (not the panel), close
if (w == m_Overlay)
{
Hide();
return true;
}
return false;
}
}Dialog Result Callbacks
For dialogs that need to return complex results, use GameScript.CallFunctionParams() with Param objects:
// Sending a result with multiple values
GetGame().GameScript.CallFunctionParams(
m_CallbackTarget,
m_CallbackFunc,
null,
new Param2<int, string>(RESULT_OK, inputText)
);
// Receiving in the caller
void OnDialogResult(int result, string text)
{
if (result == RESULT_OK)
{
ProcessInput(text);
}
}This is the same pattern VPP Admin Tools uses for its VPPDialogBox callback system.
UIScriptedWindow -- Floating Windows
DayZ has a second built-in system: UIScriptedWindow, for floating windows that exist alongside a UIScriptedMenu. Unlike UIScriptedMenu, windows are tracked in a static map and their events are routed through the active menu.
class MyWindow extends UIScriptedWindow
{
void MyWindow(int id) : UIScriptedWindow(id)
{
}
override Widget Init()
{
m_WgtRoot = GetGame().GetWorkspace().CreateWidgets(
"MyMod/GUI/layouts/my_window.layout");
return m_WgtRoot;
}
override bool OnClick(Widget w, int x, int y, int button)
{
// Handle clicks
return false;
}
}Windows are opened and closed through the UIManager:
// Open
GetGame().GetUIManager().OpenWindow(MY_WINDOW_ID);
// Close
GetGame().GetUIManager().CloseWindow(MY_WINDOW_ID);
// Check if open
GetGame().GetUIManager().IsWindowOpened(MY_WINDOW_ID);实际上, most mod developers use ScriptedWidgetEventHandler-based popups rather than UIScriptedWindow, because the window system requires registering with 引擎's switch-case in MissionBase and the events route through the active UIScriptedMenu. The manual pattern is simpler and more flexible.
常见错误
1. Not Restoring Game Focus on Close
The problem: Player cannot move, shoot, or interact after the dialog closes.
// WRONG -- no focus restoration
void CloseDialog()
{
m_Root.Unlink();
m_Root = null;
// Focus counter is still incremented!
}
// CORRECT -- always decrement
void CloseDialog()
{
m_Root.Unlink();
m_Root = null;
GetGame().GetInput().ChangeGameFocus(-1);
GetGame().GetUIManager().ShowUICursor(false);
}2. Not Unlinking Widgets on Close
The problem: Widget tree stays in memory, events keep firing, 内存泄漏s accumulate.
// WRONG -- just hiding
void Hide()
{
m_Root.Show(false); // Widget still exists and consumes memory
}
// CORRECT -- unlink destroys the widget tree
void Hide()
{
if (m_Root)
{
m_Root.Unlink();
m_Root = null;
}
}If you need to show/hide the same dialog repeatedly, keeping the widget and using Show(true/false) is fine -- just ensure you Unlink() in the destructor.
3. Dialog Renders Behind Other UI
The problem: Dialog is invisible or partially hidden because other widgets have higher rendering priority.
The fix: Use SetSort() to push the dialog above everything:
m_Root.SetSort(1024, true);4. Multiple Dialogs Stacking Focus Changes
The problem: Opening dialog A (+1), then dialog B (+1), then closing B (-1) -- focus counter is still 1, so input is still locked even though the user sees no dialog.
The fix: Track whether each dialog instance has locked focus, and only decrement if it did:
class SafeDialog : ScriptedWidgetEventHandler
{
protected bool m_HasFocus;
void LockFocus()
{
if (!m_HasFocus)
{
GetGame().GetInput().ChangeGameFocus(1);
GetGame().GetUIManager().ShowUICursor(true);
m_HasFocus = true;
}
}
void UnlockFocus()
{
if (m_HasFocus)
{
GetGame().GetInput().ChangeGameFocus(-1);
GetGame().GetUIManager().ShowUICursor(false);
m_HasFocus = false;
}
}
void ~SafeDialog()
{
UnlockFocus();
if (m_Root)
{
m_Root.Unlink();
m_Root = null;
}
}
}5. Calling Close() or Delete in the Constructor
The problem: Calling Close() or delete this during construction causes crashes or undefined behavior because the object is not fully initialized.
The fix: Defer closure using CallLater:
void MyDialog()
{
// ...
if (someErrorCondition)
{
// WRONG: Close(); or delete this;
// CORRECT:
GetGame().GetCallQueue(CALL_CATEGORY_GUI).CallLater(
DeferredClose, 0, false);
}
}
void DeferredClose()
{
Close(); // or: delete this;
}6. Not Checking for Null Before Widget Operations
The problem: Crash when accessing a widget that was already destroyed or never created.
// WRONG
void UpdateMessage(string text)
{
m_MessageText.SetText(text); // Crash if m_MessageText is null
}
// CORRECT
void UpdateMessage(string text)
{
if (m_MessageText)
m_MessageText.SetText(text);
}总结
| 方法 | 基类 | 焦点管理 | 最适合 |
|---|---|---|---|
| Engine menu stack | UIScriptedMenu | Automatic via LockControls/UnlockControls | Full-screen menus, major dialogs |
| Native dialog | ShowDialog() | Automatic | Simple Yes/No/OK prompts |
| Manual popup | ScriptedWidgetEventHandler | Manual ChangeGameFocus | In-panel popups, custom dialogs |
| Floating window | UIScriptedWindow | Via parent menu | Tool windows alongside a menu |
The golden rule: every ChangeGameFocus(1) must be matched by a ChangeGameFocus(-1). Put focus cleanup in your destructor as a safety net, always Unlink() widgets when done, and use SetSort() to ensure your dialog renders on top.
下一步
- 3.6 Event Handling -- Handle clicks, hover, keyboard events inside dialogs
- 3.5 Programmatic Widget Creation -- Build dialog content dynamically in code
