218 lines
13 KiB
C++
218 lines
13 KiB
C++
/**
|
||
* Window(头文件)
|
||
*
|
||
* 设计目标:
|
||
* - 提供一个基于 Win32 + EasyX 的“可拉伸且稳定不抖”的窗口容器。
|
||
* - 通过消息过程子类化(WndProcThunk)接管关键消息(WM_SIZING/WM_SIZE/...)。
|
||
* - 将“几何变化记录(pendingW/H)”与“统一收口重绘(needResizeDirty)”解耦。
|
||
* - 在事件分发阶段只改状态并登记重绘 root,在事件尾统一提交,减少分离绘制造成的闪烁。
|
||
*
|
||
* 关键点(与 .cpp 中实现对应):
|
||
* - isSizing:处于交互拉伸阶段时,冻结重绘;松手后统一重绘,防止抖动。
|
||
* - WM_SIZING:只做“最小尺寸夹紧”,不回滚矩形、不做对齐;把其余交给系统。
|
||
* - WM_GETMINMAXINFO:按最小“客户区”换算到“窗口矩形”,提供系统层最小轨迹值。
|
||
* - runEventLoop:输入事件先分发给对话框/控件,控件只登记重绘请求;真正绘制在分发或 resize 收口时统一处理。
|
||
*/
|
||
#pragma once
|
||
|
||
#include "Control.h"
|
||
#include <atomic>
|
||
#include <cstdint>
|
||
#include <string>
|
||
#include <vector>
|
||
#include <memory>
|
||
#include <windows.h>
|
||
|
||
class Window;
|
||
|
||
namespace StellarX::Internal
|
||
{
|
||
class NativeInputContext;
|
||
struct NativeInputRuntimeSnapshot;
|
||
NativeInputRuntimeSnapshot inspectWindowNativeInputForTesting(const ::Window& window) noexcept;
|
||
}
|
||
|
||
class Window
|
||
{
|
||
friend class Dialog;
|
||
friend StellarX::Internal::NativeInputRuntimeSnapshot
|
||
StellarX::Internal::inspectWindowNativeInputForTesting(const Window& window) noexcept;
|
||
|
||
// —— 尺寸状态 ——(绘制尺寸与待应用尺寸分离;收口时一次性更新)
|
||
int width; // 当前有效宽(已应用到画布/控件的客户区宽)
|
||
int height; // 当前有效高(已应用到画布/控件的客户区高)
|
||
int localwidth; // 基准宽(创建时的宽度)
|
||
int localheight; // 基准高(创建是的高度)
|
||
int pendingW; // 待应用宽(WM_SIZE/拉伸中记录用)
|
||
int pendingH; // 待应用高
|
||
int minClientW; // 业务设定的最小客户区宽(用于 GETMINMAXINFO 与 SIZING 夹紧)
|
||
int minClientH; // 业务设定的最小客户区高
|
||
int windowMode = NULL; // EasyX 初始化模式(EX_SHOWCONSOLE/EX_TOPMOST/...)
|
||
bool needResizeDirty = false; // 统一收口重绘标志(置位后在事件环末尾处理)
|
||
bool isSizing = false; // 是否处于拖拽阶段(ENTER/EXIT SIZEMOVE 切换)
|
||
|
||
// —— 原生窗口句柄与子类化钩子 ——(子类化 EasyX 的窗口过程以拦截关键消息)
|
||
HWND hWnd = NULL; // EasyX 初始化后的窗口句柄
|
||
WNDPROC oldWndProc = nullptr; // 保存旧过程(CallWindowProc 回落)
|
||
bool procHooked = false; // 避免重复子类化
|
||
// EasyX 的 WndProc 与宿主事件循环分属不同线程;只有 WndProc 完成 WM_NCDESTROY
|
||
// 转发后才发布 false,避免宿主提前析构仍在执行 WndProcThunk 的 Window 对象。
|
||
std::atomic<bool> nativeWindowAlive{ false };
|
||
// 阶段 1 只建立采集、规范化与跨线程队列;宿主安全点只观测排空,不做业务路由。
|
||
std::unique_ptr<StellarX::Internal::NativeInputContext> nativeInputContext;
|
||
bool nativeInputFailureReported = false;
|
||
std::uint64_t nativeInputRejectedTextReported = 0;
|
||
std::uint64_t nativeInputImeQueryFailuresReported = 0;
|
||
std::uint64_t nativeInputDiscardedFragmentsReported = 0;
|
||
bool hasLiveNativeWindow() const noexcept;
|
||
bool drainNativeInputAtSafePoint();
|
||
static LRESULT CALLBACK WndProcThunk(HWND h, UINT m, WPARAM w, LPARAM l); // 静态过程分发到 this
|
||
|
||
// —— 绘制相关 ——(是否使用合成双缓冲、窗口标题、背景等)
|
||
bool useComposited = true; // 是否启用 WS_EX_COMPOSITED(部分机器可能增加一帧观感延迟)
|
||
std::string headline; // 窗口标题文本
|
||
COLORREF wBkcolor = BLACK; // 纯色背景(无背景图时使用)
|
||
std::unique_ptr<IMAGE> background; // 背景图对象指针(存在时优先绘制)
|
||
std::string bkImageFile; // 背景图文件路径(loadimage 用)
|
||
|
||
// —— 控件/对话框 ——(容器内的普通控件与非模态对话框)
|
||
std::vector<std::unique_ptr<Control>> controls; // 普通顶层控件;绘制顺序也决定层级顺序
|
||
std::vector<std::unique_ptr<Control>> dialogs; // 非模态对话框;始终位于普通控件之上
|
||
// 顶层控件的结构变更与事件分发解耦,防止回调直接改动正在遍历的 controls。
|
||
// add/remove/clear 在事件、绘制和回调期间只入队;最外层 mutation scope 结束后再提交。
|
||
enum class ControlMutationKind { Add, Remove, Clear };
|
||
struct PendingControlMutation
|
||
{
|
||
ControlMutationKind kind = ControlMutationKind::Clear;
|
||
StellarX::ControlHandle handle{};
|
||
std::unique_ptr<Control> control;
|
||
};
|
||
std::vector<PendingControlMutation> pendingControlMutations;
|
||
int mutationScopeDepth = 0; // 允许嵌套的结构变更事务深度
|
||
// 队列提交期间保持该标志,避免提交过程中再次递归提交同一队列。
|
||
bool applyingControlMutations = false;
|
||
bool fullSceneRedrawRequested = false; // 非局部变化请求整场景重绘;在事件收口安全点消费
|
||
bool managedDispatchActive = false; // 事件分发期:控件只改状态并登记重绘,不立即画
|
||
bool managedSceneDirty = false; // 当前分发轮次是否已经登记了至少一笔托管重绘请求
|
||
struct ManagedRepaintItem // 托管重绘项:记录由哪个控件发起、需要重绘的根控件和覆盖范围(用于后续判断哪些对话框需要补画)
|
||
{
|
||
Control* root = nullptr; // 顶层重绘根(直接挂在 Window 下的控件,或 Dialog 自身)
|
||
RECT coverage{}; // 本轮脏区覆盖范围;用于判断哪些上层 Dialog 需要补画
|
||
};
|
||
std::vector<ManagedRepaintItem> managedRepaintItems; // 本轮事件分发累计的重绘项
|
||
|
||
public:
|
||
bool dialogClose = false; // 项目内使用的状态位,对话框关闭标志
|
||
mutable bool dialogOpen = false; // 项目内使用的状态位,对话框打开标志
|
||
|
||
// —— 构造/析构 ——(仅初始化成员;实际样式与子类化在 draw() 中完成)
|
||
// 创建纯色背景窗口
|
||
Window(int width, int height, int mode);
|
||
// 创建指定背景色窗口
|
||
Window(int width, int height, int mode, COLORREF bkcloc);
|
||
// 创建带标题与背景色的窗口
|
||
Window(int width, int height, int mode, COLORREF bkcloc, std::string headline);
|
||
// 析构窗口并释放背景、控件与子类化资源
|
||
~Window();
|
||
|
||
// —— 绘制与事件循环 ——(draw* 完成一次全量绘制;runEventLoop 驱动事件与统一收口)
|
||
void draw(); // 以纯色背景执行一次全量绘制
|
||
void draw(std::string pImgFile); // 以背景图执行一次全量绘制
|
||
int runEventLoop(); // 主事件循环(PeekMessage + 统一收口重绘)
|
||
|
||
// —— 背景/标题设置 ——(更换背景、背景色与标题;立即触发一次批量绘制)
|
||
void setBkImage(std::string pImgFile); // 设置窗口背景图
|
||
void setBkcolor(COLORREF c); // 设置窗口纯色背景
|
||
void setHeadline(std::string headline); // 设置窗口标题
|
||
|
||
// —— 控件/对话框管理 ——(添加到容器,或做存在性判断)
|
||
// 添加普通顶层控件并返回稳定句柄。
|
||
// 在事件回调、绘制或其它 mutation transaction 中调用时,control 会先进入待提交队列;
|
||
// 调用方仍可立即保存返回句柄,但对象要到最外层事务结束后才真正出现在 controls 中。
|
||
StellarX::ControlHandle addControl(std::unique_ptr<Control> control);
|
||
// 按句柄移除顶层控件;事务期间延迟删除,避免遍历中的对象或迭代器失效。
|
||
// 返回 true 表示目标命中且删除请求已排队/提交;返回 false 表示句柄无效或未命中。
|
||
bool removeControl(StellarX::ControlHandle handle);
|
||
// 清空所有普通顶层控件;事务期间延迟到安全点提交。
|
||
void clearControls();
|
||
// 查找当前已提交的控件;不会暴露 pending 队列中的对象。
|
||
Control* findControl(StellarX::ControlHandle handle);
|
||
// 查找当前已提交控件的只读视图;不会暴露 pending 队列中的对象。
|
||
const Control* findControl(StellarX::ControlHandle handle) const;
|
||
// 返回当前已提交控件的句柄快照;调用方不得据此推断控件之后仍然存在。
|
||
std::vector<StellarX::ControlHandle> getControlHandles() const;
|
||
// 添加非模态对话框;Window 接管生命周期并在事件安全点清理关闭对象。
|
||
void addDialog(std::unique_ptr<Control> dialogs);
|
||
//非模态对话框去重
|
||
bool hasNonModalDialogWithCaption(const std::string& caption, const std::string& message) const; // 检查是否已有同 caption/message 的非模态对话框
|
||
|
||
// —— 访问器 ——(只读接口,供外部查询当前窗口/标题/背景等)
|
||
HWND getHwnd() const; // 获取窗口句柄
|
||
int getWidth() const; // 获取当前有效客户区宽度
|
||
int getHeight() const; // 获取当前有效客户区高度
|
||
int getPendingWidth() const; // 获取待应用宽度
|
||
int getPendingHeight() const; // 获取待应用高度
|
||
std::string getHeadline() const; // 获取窗口标题
|
||
COLORREF getBkcolor() const; // 获取纯色背景
|
||
IMAGE* getBkImage() const; // 获取背景图对象
|
||
std::string getBkImageFile() const; // 获取背景图路径
|
||
|
||
// —— 尺寸调整 / 托管重绘 ——(事件阶段登记,收口阶段提交)
|
||
|
||
// 执行一次 resize 收口 + 统一重绘;只在 pending 尺寸或全场景脏标记存在时真正绘制。
|
||
void pumpResizeIfNeeded();
|
||
// 供模态 Dialog 上报宿主窗口尺寸变化;只记录待应用尺寸,实际布局/重绘由窗口收口统一完成。
|
||
void scheduleResizeFromModal(int w, int h);
|
||
// 当前是否处于“事件只改状态,不立即画”的阶段。
|
||
// 控件在该阶段应请求托管重绘,而不是直接回贴背景或立即 draw。
|
||
bool isManagedDispatchActive() const;
|
||
// 记录一笔由 source 发起的托管重绘请求。
|
||
// Window 会提升到对应 root,并在事件收口时合并 coverage 后统一提交。
|
||
void requestManagedRepaint(Control* source);
|
||
// 记录一笔托管重绘请求,并显式并入状态清理前的旧覆盖范围。
|
||
// 用于 hover/tooltip 等状态切换,保证旧区域和新区域都会被清理。
|
||
void requestManagedRepaint(Control* source, const RECT& previousCoverage);
|
||
// 在事件收口阶段提交本轮登记的 root 重绘;会补画覆盖在脏区之上的非模态对话框。
|
||
void flushManagedRepaint();
|
||
// 进入结构变更事务。允许嵌套,调用者必须保证 begin/end 成对。
|
||
// 事务内的顶层控件变更只入队,不立即修改 controls/dialogs。
|
||
void beginMutationScope();
|
||
// 离开结构变更事务。最外层退出且当前不在托管分发阶段时,会提交整棵控件树的 pending mutation。
|
||
void endMutationScope();
|
||
// 判断当前是否处于不允许立即改动控件树的阶段,包括事件分发、显式事务或队列提交过程。
|
||
bool isMutationTransactionActive() const;
|
||
// 请求下一安全点执行整场景重绘;用于 Dialog 关闭、全局覆盖关系变化等无法安全局部修复的场景。
|
||
void requestFullSceneRedraw();
|
||
private:
|
||
// 提交 Window 顶层控件队列;只允许在最外层事务安全点调用。
|
||
void flushPendingControlMutations();
|
||
// 自顶向下提交 Window、Canvas、TabControl 等容器中累积的 pending mutation。
|
||
void flushPendingMutationsInTree();
|
||
// 将全部普通控件和非模态对话框标脏,用于强制全场景重绘前统一失效缓存。
|
||
void markAllControlsDirty();
|
||
// 根据最终窗口尺寸和控件布局规格,解算并应用控件运行态矩形。
|
||
void adaptiveLayout(std::unique_ptr<Control>& c, const int finalH, const int finalW);
|
||
// resize / 初次绘制 / 对话框开关这类全局场景的整场景重绘
|
||
void redrawScene(bool forceControlsDirty, bool forceDialogsDirty);
|
||
// 绘制窗口背景;存在背景图时优先贴图,否则填充纯色背景。
|
||
void drawWindowBackground();
|
||
// 合成 WM_MOUSEMOVE,用于同步底层 hover 状态
|
||
void dispatchSyntheticMouseMoveToControls(short x, short y);
|
||
enum class OverlayMouseSyncMode
|
||
{
|
||
ClearBehindOverlay, // overlay 吞掉鼠标移动:只清理底层 hover / tooltip,不重新命中底层控件
|
||
RestoreAtCursor // overlay 关闭:按当前鼠标位置刷新底层 hover 状态
|
||
};
|
||
// overlay 层级变化后,同步底层鼠标瞬时状态;内部仍复用既有 synthetic WM_MOUSEMOVE 路径
|
||
void syncMouseStateAfterOverlayChanged(OverlayMouseSyncMode mode);
|
||
// 在事件安全点清理已关闭的非模态 Dialog,避免 dialogs 容器长期积累无效对象。
|
||
// 清理可能触发全场景重绘请求,因此必须放在遍历之外。
|
||
void sweepClosedDialogs();
|
||
// 事件回调可能改脏其他 root;flush 前补收集这些跨 root 脏子树
|
||
void collectDirtyRootsForManagedRepaint();
|
||
// 清空本轮托管重绘登记
|
||
void clearManagedRepaintState();
|
||
// 找出需要补画到最上层的对话框
|
||
void collectManagedDialogOverlays(Control* repaintRoot, const RECT& coverage, std::vector<Control*>& overlays);
|
||
};
|