完成 StellarX 核心重构、缺陷修复与测试收口
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
.vs/
|
||||
x64/
|
||||
.codex-temp/
|
||||
.codex-staging/
|
||||
build/
|
||||
TestResults/
|
||||
*.vcxproj.user
|
||||
*.obj
|
||||
*.exe
|
||||
*.pdb
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\run-key.ps1"
|
||||
set "runnerExit=%ERRORLEVEL%"
|
||||
|
||||
if not "%runnerExit%"=="0" (
|
||||
echo.
|
||||
echo KEY test launcher failed with exit code %runnerExit%.
|
||||
pause
|
||||
)
|
||||
|
||||
exit /b %runnerExit%
|
||||
@@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<!-- Build the reusable StellarX framework implementation as a static library. -->
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<!-- Keep the library build matrix aligned with the consuming app and test projects. -->
|
||||
<ProjectConfiguration Include="Debug|Win32"><Configuration>Debug</Configuration><Platform>Win32</Platform></ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|Win32"><Configuration>Release</Configuration><Platform>Win32</Platform></ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug|x64"><Configuration>Debug</Configuration><Platform>x64</Platform></ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64"><Configuration>Release</Configuration><Platform>x64</Platform></ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>17.0</VCProjectVersion>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<ProjectGuid>{28B8D78E-85AF-4F14-AD56-D51AB6958941}</ProjectGuid>
|
||||
<RootNamespace>StellarXCore</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Label="Configuration">
|
||||
<!-- The .lib is linked into executables at build time; no runtime DLL is produced. -->
|
||||
<ConfigurationType>StaticLibrary</ConfigurationType>
|
||||
<UseDebugLibraries Condition="'$(Configuration)'=='Debug'">true</UseDebugLibraries>
|
||||
<UseDebugLibraries Condition="'$(Configuration)'=='Release'">false</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<CharacterSet>MultiByte</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<PropertyGroup>
|
||||
<!-- Normal and sanitizer outputs use separate directories so ASan builds stay isolated. -->
|
||||
<OutDir>$(SolutionDir)build\$(Platform)\$(Configuration)\</OutDir>
|
||||
<IntDir>$(SolutionDir)build\obj\$(ProjectName)\$(Platform)\$(Configuration)\</IntDir>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(EnableAsan)'=='true' And '$(Platform)'=='x64'">
|
||||
<EnableASAN>true</EnableASAN>
|
||||
<OutDir>$(ProjectDir)build\$(Platform)\$(Configuration)-asan\</OutDir>
|
||||
<IntDir>$(ProjectDir)build\obj\$(ProjectName)\$(Platform)\$(Configuration)-asan\</IntDir>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<WarningLevel>Level4</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
<AdditionalIncludeDirectories>$(ProjectDir)include\StellarX;$(ProjectDir)src\StellarX\internal;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<!-- Public and internal headers are kept in this project so all consumers use one implementation. -->
|
||||
<ItemGroup>
|
||||
<ClInclude Include="include\StellarX\Button.h" /><ClInclude Include="include\StellarX\Canvas.h" /><ClInclude Include="include\StellarX\Control.h" />
|
||||
<ClInclude Include="include\StellarX\CoreTypes.h" /><ClInclude Include="include\StellarX\Dialog.h" /><ClInclude Include="include\StellarX\Label.h" />
|
||||
<ClInclude Include="include\StellarX\MessageBox.h" /><ClInclude Include="include\StellarX\StellarX.h" /><ClInclude Include="src\StellarX\internal\SxEventLoopInternal.h" /><ClInclude Include="include\StellarX\SxLog.h" /><ClInclude Include="src\StellarX\internal\SxTextInternal.h" />
|
||||
<ClInclude Include="include\StellarX\TabControl.h" /><ClInclude Include="include\StellarX\Table.h" /><ClInclude Include="include\StellarX\TextBox.h" /><ClInclude Include="include\StellarX\Window.h" />
|
||||
</ItemGroup>
|
||||
<!-- Framework source files compiled into StellarX.Core.lib. -->
|
||||
<ItemGroup>
|
||||
<ClCompile Include="src\StellarX\Button.cpp" /><ClCompile Include="src\StellarX\Canvas.cpp" /><ClCompile Include="src\StellarX\Control.cpp" />
|
||||
<ClCompile Include="src\StellarX\Dialog.cpp" /><ClCompile Include="src\StellarX\Label.cpp" /><ClCompile Include="src\StellarX\MessageBox.cpp" />
|
||||
<ClCompile Include="src\StellarX\SxLog.cpp" /><ClCompile Include="src\StellarX\TabControl.cpp" /><ClCompile Include="src\StellarX\Table.cpp" />
|
||||
<ClCompile Include="src\StellarX\TextBox.cpp" /><ClCompile Include="src\StellarX\Window.cpp" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
</Project>
|
||||
@@ -53,7 +53,7 @@
|
||||
#include"StellarX.h"
|
||||
#include <vector>
|
||||
#ifndef KEY
|
||||
#define KEY 6
|
||||
#define KEY 1
|
||||
#endif
|
||||
|
||||
// 测试用例统一使用最新布局 API,避免继续依赖旧 setAnchor() 兼容层。
|
||||
@@ -285,8 +285,9 @@ int main()
|
||||
rightPage2Ptr->addControl(std::move(rightPage2Label));
|
||||
rightPage2Ptr->addControl(std::move(roundReadOnly));
|
||||
|
||||
rightTabsPtr->add(std::make_pair(std::make_unique<Button>(0, 0, 76, 34, "页一"), std::move(rightPage1)));
|
||||
rightTabsPtr->add(std::make_pair(std::make_unique<Button>(0, 0, 76, 34, "页二"), std::move(rightPage2)));
|
||||
// 使用新的 addTab 所有权接口,分别传入页签按钮和页面对象。
|
||||
rightTabsPtr->addTab(std::make_unique<Button>(0, 0, 76, 34, "页一"), std::move(rightPage1));
|
||||
rightTabsPtr->addTab(std::make_unique<Button>(0, 0, 76, 34, "页二"), std::move(rightPage2));
|
||||
rightTabsPtr->setActiveIndex(0);
|
||||
|
||||
tabPanelPtr->addControl(std::move(tabTitle));
|
||||
@@ -561,8 +562,9 @@ int main()
|
||||
auto tabBtn2 = std::make_unique<Button>(0, 0, 108, 30, "交互页");
|
||||
configureTooltip(tabBtn1.get(), "布局页页签按钮");
|
||||
configureTooltip(tabBtn2.get(), "交互页页签按钮");
|
||||
tabControlPtr->add(std::make_pair(std::move(tabBtn1), std::move(tabPage1)));
|
||||
tabControlPtr->add(std::make_pair(std::move(tabBtn2), std::move(tabPage2)));
|
||||
// 页签标题允许由调用方重复,但激活身份始终由索引决定。
|
||||
tabControlPtr->addTab(std::move(tabBtn1), std::move(tabPage1));
|
||||
tabControlPtr->addTab(std::move(tabBtn2), std::move(tabPage2));
|
||||
tabControlPtr->setActiveIndex(0);
|
||||
|
||||
tabZonePtr->addControl(std::move(tabTitle));
|
||||
@@ -812,9 +814,10 @@ int main()
|
||||
|
||||
});
|
||||
p.second->addControl(std::move(b));
|
||||
ta->add(std::move(p));
|
||||
ta->add(std::move(p1));
|
||||
ta->add(std::move(p2));
|
||||
// pair 仅用于组装示例数据,交给 addTab 时拆开并转移两个对象的所有权。
|
||||
ta->addTab(std::move(p.first), std::move(p.second));
|
||||
ta->addTab(std::move(p1.first), std::move(p1.second));
|
||||
ta->addTab(std::move(p2.first), std::move(p2.second));
|
||||
ta->setTabBarHeight(20);
|
||||
ta->setCanvasfillMode(StellarX::FillMode::Null);
|
||||
auto ta_ptr = ta.get();
|
||||
@@ -925,9 +928,10 @@ int main()
|
||||
auto tabPage2Ptr = page2.get();
|
||||
auto tabPage3Ptr = page3.get();
|
||||
|
||||
tabControlPtr->add(std::make_pair(std::make_unique<Button>(0, 0, 120, 28, "页签-事件区"), std::move(page1)));
|
||||
tabControlPtr->add(std::make_pair(std::make_unique<Button>(0, 0, 120, 28, "页签-空白区"), std::move(page2)));
|
||||
tabControlPtr->add(std::make_pair(std::make_unique<Button>(0, 0, 120, 28, "页签-辅助区"), std::move(page3)));
|
||||
// 事件场景同样使用按索引管理的页签 API,避免旧的 pair 重载继续被调用。
|
||||
tabControlPtr->addTab(std::make_unique<Button>(0, 0, 120, 28, "页签-事件区"), std::move(page1));
|
||||
tabControlPtr->addTab(std::make_unique<Button>(0, 0, 120, 28, "页签-空白区"), std::move(page2));
|
||||
tabControlPtr->addTab(std::make_unique<Button>(0, 0, 120, 28, "页签-辅助区"), std::move(page3));
|
||||
tabControlPtr->setActiveIndex(0);
|
||||
|
||||
auto page1Label = std::make_unique<Label>(20, 10, "区域 B:打开非模态对话框后,切换页签并在页内按钮上快速 Hover。");
|
||||
@@ -1561,7 +1565,8 @@ int main()
|
||||
for (auto& log : logIn_label)
|
||||
{
|
||||
log->setTextdisap(true);
|
||||
log->textStyle.lpszFace = "华文行楷";
|
||||
// 字体名现在由 ControlText 自己拥有,直接赋值字符串即可安全保存。
|
||||
log->textStyle.faceName = "华文行楷";
|
||||
}
|
||||
logIn_label[0]->textStyle.nHeight = 100;
|
||||
SxTestSetTopFixed(logIn_label[0].get());
|
||||
@@ -1621,7 +1626,7 @@ int main()
|
||||
//添加页签及页
|
||||
auto tabP = std::make_pair(std::make_unique<Button>(0, 0, 100, 100, "点餐"), std::make_unique<Canvas>(0, 0, 1290, 790));
|
||||
tabP.second->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
|
||||
tabControl_ptr->add(std::move(tabP));
|
||||
tabControl_ptr->addTab(std::move(tabP.first), std::move(tabP.second));
|
||||
|
||||
/*------------login事件-------------*/
|
||||
//管理员登录按钮事件
|
||||
@@ -5,6 +5,12 @@ VisualStudioVersion = 17.13.35828.75 d17.13
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "imGui-easyX", "imGui-easyX.vcxproj", "{FD33B55B-FD97-4087-8B95-6BF8285AAE73}"
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "StellarX.Core", "StellarX.Core.vcxproj", "{28B8D78E-85AF-4F14-AD56-D51AB6958941}"
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "StellarX.Tests", "tests\StellarX.Tests.vcxproj", "{8779C678-AD5B-4828-A4A8-57D4C4254733}"
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "StellarX.UiFixture", "tests\StellarX.UiFixture.vcxproj", "{D0AF4B0B-7379-4A20-BF10-68CC8DA4D905}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|x64 = Debug|x64
|
||||
@@ -21,6 +27,30 @@ Global
|
||||
{FD33B55B-FD97-4087-8B95-6BF8285AAE73}.Release|x64.Build.0 = Release|x64
|
||||
{FD33B55B-FD97-4087-8B95-6BF8285AAE73}.Release|x86.ActiveCfg = Release|Win32
|
||||
{FD33B55B-FD97-4087-8B95-6BF8285AAE73}.Release|x86.Build.0 = Release|Win32
|
||||
{28B8D78E-85AF-4F14-AD56-D51AB6958941}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{28B8D78E-85AF-4F14-AD56-D51AB6958941}.Debug|x64.Build.0 = Debug|x64
|
||||
{28B8D78E-85AF-4F14-AD56-D51AB6958941}.Debug|x86.ActiveCfg = Debug|Win32
|
||||
{28B8D78E-85AF-4F14-AD56-D51AB6958941}.Debug|x86.Build.0 = Debug|Win32
|
||||
{28B8D78E-85AF-4F14-AD56-D51AB6958941}.Release|x64.ActiveCfg = Release|x64
|
||||
{28B8D78E-85AF-4F14-AD56-D51AB6958941}.Release|x64.Build.0 = Release|x64
|
||||
{28B8D78E-85AF-4F14-AD56-D51AB6958941}.Release|x86.ActiveCfg = Release|Win32
|
||||
{28B8D78E-85AF-4F14-AD56-D51AB6958941}.Release|x86.Build.0 = Release|Win32
|
||||
{8779C678-AD5B-4828-A4A8-57D4C4254733}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{8779C678-AD5B-4828-A4A8-57D4C4254733}.Debug|x64.Build.0 = Debug|x64
|
||||
{8779C678-AD5B-4828-A4A8-57D4C4254733}.Debug|x86.ActiveCfg = Debug|Win32
|
||||
{8779C678-AD5B-4828-A4A8-57D4C4254733}.Debug|x86.Build.0 = Debug|Win32
|
||||
{8779C678-AD5B-4828-A4A8-57D4C4254733}.Release|x64.ActiveCfg = Release|x64
|
||||
{8779C678-AD5B-4828-A4A8-57D4C4254733}.Release|x64.Build.0 = Release|x64
|
||||
{8779C678-AD5B-4828-A4A8-57D4C4254733}.Release|x86.ActiveCfg = Release|Win32
|
||||
{8779C678-AD5B-4828-A4A8-57D4C4254733}.Release|x86.Build.0 = Release|Win32
|
||||
{D0AF4B0B-7379-4A20-BF10-68CC8DA4D905}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{D0AF4B0B-7379-4A20-BF10-68CC8DA4D905}.Debug|x64.Build.0 = Debug|x64
|
||||
{D0AF4B0B-7379-4A20-BF10-68CC8DA4D905}.Debug|x86.ActiveCfg = Debug|Win32
|
||||
{D0AF4B0B-7379-4A20-BF10-68CC8DA4D905}.Debug|x86.Build.0 = Debug|Win32
|
||||
{D0AF4B0B-7379-4A20-BF10-68CC8DA4D905}.Release|x64.ActiveCfg = Release|x64
|
||||
{D0AF4B0B-7379-4A20-BF10-68CC8DA4D905}.Release|x64.Build.0 = Release|x64
|
||||
{D0AF4B0B-7379-4A20-BF10-68CC8DA4D905}.Release|x86.ActiveCfg = Release|Win32
|
||||
{D0AF4B0B-7379-4A20-BF10-68CC8DA4D905}.Release|x86.Build.0 = Release|Win32
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
+33
-33
@@ -19,33 +19,27 @@
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="Button.h" />
|
||||
<ClInclude Include="Canvas.h" />
|
||||
<ClInclude Include="Control.h" />
|
||||
<ClInclude Include="CoreTypes.h" />
|
||||
<ClInclude Include="Dialog.h" />
|
||||
<ClInclude Include="label.h" />
|
||||
<ClInclude Include="MessageBox.h" />
|
||||
<ClInclude Include="StellarX.h" />
|
||||
<ClInclude Include="SxLog.h" />
|
||||
<ClInclude Include="TabControl.h" />
|
||||
<ClInclude Include="table.h" />
|
||||
<ClInclude Include="textBox.h" />
|
||||
<ClInclude Include="window.h" />
|
||||
<ClInclude Include="include\StellarX\Button.h" />
|
||||
<ClInclude Include="include\StellarX\Canvas.h" />
|
||||
<ClInclude Include="include\StellarX\Control.h" />
|
||||
<ClInclude Include="include\StellarX\CoreTypes.h" />
|
||||
<ClInclude Include="include\StellarX\Dialog.h" />
|
||||
<ClInclude Include="include\StellarX\Label.h" />
|
||||
<ClInclude Include="include\StellarX\MessageBox.h" />
|
||||
<ClInclude Include="include\StellarX\StellarX.h" />
|
||||
<ClInclude Include="include\StellarX\SxLog.h" />
|
||||
<ClInclude Include="include\StellarX\TabControl.h" />
|
||||
<ClInclude Include="include\StellarX\Table.h" />
|
||||
<ClInclude Include="include\StellarX\TextBox.h" />
|
||||
<ClInclude Include="include\StellarX\Window.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="Button.cpp" />
|
||||
<ClCompile Include="Canvas.cpp" />
|
||||
<ClCompile Include="Control.cpp" />
|
||||
<ClCompile Include="Dialog.cpp" />
|
||||
<ClCompile Include="label.cpp" />
|
||||
<ClCompile Include="MessageBox.cpp" />
|
||||
<ClCompile Include="SxLog.cpp" />
|
||||
<ClCompile Include="TabControl.cpp" />
|
||||
<ClCompile Include="table.cpp" />
|
||||
<ClCompile Include="textBox.cpp" />
|
||||
<ClCompile Include="window.cpp" />
|
||||
<ClCompile Include="z-testDome.cpp" />
|
||||
<ClCompile Include="app\z-testDome.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="StellarX.Core.vcxproj">
|
||||
<Project>{28B8D78E-85AF-4F14-AD56-D51AB6958941}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>17.0</VCProjectVersion>
|
||||
@@ -82,6 +76,11 @@
|
||||
<CharacterSet>MultiByte</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>$(ProjectDir)include\StellarX;$(ProjectDir)src\StellarX\internal;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="Shared">
|
||||
@@ -99,12 +98,16 @@
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup>
|
||||
<KeyScenario Condition="'$(KeyScenario)'==''">6</KeyScenario>
|
||||
<OutDir>$(SolutionDir)build\$(Platform)\$(Configuration)\</OutDir>
|
||||
<IntDir>$(SolutionDir)build\obj\$(ProjectName)\$(Platform)\$(Configuration)\</IntDir>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>false</SDLCheck>
|
||||
<PreprocessorDefinitions>
|
||||
</PreprocessorDefinitions>
|
||||
<PreprocessorDefinitions>KEY=$(KeyScenario);%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
<PrecompiledHeaderFile />
|
||||
@@ -120,8 +123,7 @@
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<SDLCheck>false</SDLCheck>
|
||||
<PreprocessorDefinitions>
|
||||
</PreprocessorDefinitions>
|
||||
<PreprocessorDefinitions>KEY=$(KeyScenario);%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
<PrecompiledHeaderFile />
|
||||
@@ -137,8 +139,7 @@
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>false</SDLCheck>
|
||||
<PreprocessorDefinitions>
|
||||
</PreprocessorDefinitions>
|
||||
<PreprocessorDefinitions>KEY=$(KeyScenario);%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
<PrecompiledHeaderFile />
|
||||
@@ -155,8 +156,7 @@
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<SDLCheck>false</SDLCheck>
|
||||
<PreprocessorDefinitions>
|
||||
</PreprocessorDefinitions>
|
||||
<PreprocessorDefinitions>KEY=$(KeyScenario);%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
<PrecompiledHeaderFile />
|
||||
|
||||
+14
-47
@@ -15,81 +15,48 @@
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="Control.h">
|
||||
<ClInclude Include="include\StellarX\Control.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="Button.h">
|
||||
<ClInclude Include="include\StellarX\Button.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="window.h">
|
||||
<ClInclude Include="include\StellarX\Window.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="label.h">
|
||||
<ClInclude Include="include\StellarX\Label.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="textBox.h">
|
||||
<ClInclude Include="include\StellarX\TextBox.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="Canvas.h">
|
||||
<ClInclude Include="include\StellarX\Canvas.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="table.h">
|
||||
<ClInclude Include="include\StellarX\Table.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="StellarX.h">
|
||||
<ClInclude Include="include\StellarX\StellarX.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="CoreTypes.h">
|
||||
<ClInclude Include="include\StellarX\CoreTypes.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="Dialog.h">
|
||||
<ClInclude Include="include\StellarX\Dialog.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="MessageBox.h">
|
||||
<ClInclude Include="include\StellarX\MessageBox.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="TabControl.h">
|
||||
<ClInclude Include="include\StellarX\TabControl.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="SxLog.h">
|
||||
<ClInclude Include="include\StellarX\SxLog.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="Control.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Button.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="z-testDome.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="window.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="label.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="textBox.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Canvas.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="table.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Dialog.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="MessageBox.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="TabControl.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="SxLog.cpp">
|
||||
<ClCompile Include="app\z-testDome.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
|
||||
@@ -55,6 +55,12 @@ class Button : public Control
|
||||
std::function<void()> onToggleOnCallback; //TOGGLE模式下的回调函数
|
||||
std::function<void()> onToggleOffCallback; //TOGGLE模式下的回调函数
|
||||
|
||||
public:
|
||||
// 必须先构造公开字体样式,再用它初始化旧样式快照。
|
||||
// C++ 按成员声明顺序构造;若顺序反过来,oldStyle 会读取尚未构造的 std::string。
|
||||
StellarX::ControlText textStyle; // 按钮文字样式
|
||||
|
||||
private:
|
||||
StellarX::ControlText oldStyle = textStyle; // 按钮文字样式
|
||||
int oldtext_width = -1;
|
||||
int oldtext_height = -1;
|
||||
@@ -78,32 +84,32 @@ class Button : public Control
|
||||
std::string tipTextOff; // click==false 时用
|
||||
Label tipLabel; // 直接复用Label作为提示
|
||||
|
||||
public:
|
||||
StellarX::ControlText textStyle; // 按钮文字样式
|
||||
public:
|
||||
|
||||
public:
|
||||
|
||||
//默认按钮颜色
|
||||
// 创建使用默认颜色的按钮。
|
||||
Button(int x, int y, int width, int height, const std::string text,
|
||||
StellarX::ButtonMode mode = StellarX::ButtonMode::NORMAL, StellarX::ControlShape shape = StellarX::ControlShape::RECTANGLE);
|
||||
//自定义按钮未被点击和被点击颜色
|
||||
// 创建自定义默认态和点击态颜色的按钮。
|
||||
Button(int x, int y, int width, int height, const std::string text,
|
||||
COLORREF ct, COLORREF cf, StellarX::ButtonMode mode = StellarX::ButtonMode::NORMAL,
|
||||
StellarX::ControlShape shape = StellarX::ControlShape::RECTANGLE);
|
||||
//自定义按钮颜色和悬停颜色
|
||||
// 创建自定义默认态、点击态和悬停态颜色的按钮。
|
||||
Button(int x, int y, int width, int height, const std::string text,
|
||||
COLORREF ct, COLORREF cf, COLORREF ch,
|
||||
StellarX::ButtonMode mode = StellarX::ButtonMode::NORMAL, StellarX::ControlShape shape = StellarX::ControlShape::RECTANGLE);
|
||||
//析构函数 释放图形指针内存
|
||||
// 析构按钮并释放图像填充资源。
|
||||
~Button();
|
||||
|
||||
//绘制按钮
|
||||
// 绘制按钮本体、文本和必要时的 Tooltip。
|
||||
void draw() override;
|
||||
//按钮事件处理
|
||||
// 处理按钮点击、悬停、切换状态和 Tooltip 触发。
|
||||
bool handleEvent(const ExMessage& msg) override;
|
||||
// 由窗口时钟推进 Tooltip 到期状态,即使没有新的鼠标消息也能触发显示。
|
||||
void tick(ULONGLONG nowMs) override;
|
||||
// 清理按钮的鼠标瞬时状态;用于父容器在 WM_MOUSEMOVE 短路后补做 hover/tooltip 收口。
|
||||
bool clearTransientMouseState() override;
|
||||
// Tooltip 可见时,按钮实际写像素范围不再等于按钮本体,需要把 Tooltip 矩形并入 coverage。
|
||||
// coverage 必须覆盖按钮本体和可见 Tooltip,才能正确恢复旧区域。
|
||||
RECT getManagedRepaintCoverageRect() const override;
|
||||
// Button 的持久绘制范围只包含本体;Tooltip 是临时浮层,不能污染兄弟控件背景快照。
|
||||
RECT getManagedRepaintPersistentCoverageRect() const override;
|
||||
@@ -130,53 +136,60 @@ public:
|
||||
void setButtonBorder(COLORREF Border);
|
||||
// 设置按钮默认态颜色
|
||||
void setButtonFalseColor(COLORREF color);
|
||||
// 设置按钮文本(char* 重载)
|
||||
// 设置按钮文本(char* 重载)。
|
||||
// 会重新计算文本像素尺寸并让自动 Tooltip 文案同步为新文本;传入指针内容会被拷贝。
|
||||
void setButtonText(const char* text);
|
||||
// 设置按钮文本(std::string 重载)
|
||||
// 设置按钮文本(std::string 重载)。
|
||||
// 若用户未显式覆盖 Tooltip 文案,默认 Tooltip 也会跟随按钮文本更新。
|
||||
void setButtonText(std::string text);
|
||||
// 设置按钮几何形状
|
||||
// 设置按钮几何形状。
|
||||
void setButtonShape(StellarX::ControlShape shape);
|
||||
// 直接设置按钮点击状态;NORMAL 设置为 true 时触发 onClick,TOGGLE 仅在状态变化时触发 on/off 回调
|
||||
// 直接设置按钮点击状态。
|
||||
// NORMAL 设置为 true 时触发 onClick;TOGGLE 仅在状态实际变化时触发 on/off 回调,重复设置同值不会重跑回调。
|
||||
// 回调在 mutation scope 内执行,可以安全排队修改宿主控件树。
|
||||
void setButtonClick(BOOL click);
|
||||
|
||||
// 查询按钮当前是否处于点击/选中状态
|
||||
// 查询按钮当前是否处于点击/选中状态。
|
||||
bool isClicked() const;
|
||||
|
||||
// 获取按钮文本
|
||||
// 获取按钮文本。
|
||||
std::string getButtonText() const;
|
||||
// 获取按钮文本的 C 字符串视图
|
||||
// 获取按钮文本的 C 字符串视图;指针仅在下一次修改按钮文本前有效,不应长期保存。
|
||||
const char* getButtonText_c() const;
|
||||
// 获取按钮模式
|
||||
// 获取按钮模式。
|
||||
StellarX::ButtonMode getButtonMode() const;
|
||||
// 获取按钮形状
|
||||
// 获取按钮形状。
|
||||
StellarX::ControlShape getButtonShape() const;
|
||||
// 获取按钮填充模式
|
||||
// 获取按钮填充模式。
|
||||
StellarX::FillMode getFillMode() const;
|
||||
// 获取按钮图案填充样式
|
||||
// 获取按钮图案填充样式。
|
||||
StellarX::FillStyle getFillIma() const;
|
||||
// 获取按钮图像填充资源
|
||||
// 获取按钮图像填充资源;未设置图像时返回 nullptr。
|
||||
IMAGE* getFillImaImage() const;
|
||||
// 获取按钮边框颜色
|
||||
// 获取按钮边框颜色。
|
||||
COLORREF getButtonBorder() const;
|
||||
// 获取按钮文字颜色
|
||||
// 获取按钮文字颜色。
|
||||
COLORREF getButtonTextColor() const;
|
||||
// 获取按钮文字样式
|
||||
// 获取按钮文字样式。
|
||||
StellarX::ControlText getButtonTextStyle() const;
|
||||
public:
|
||||
// === Tooltip API===
|
||||
// 开关 Tooltip;关闭时只修改内部启用/显示状态,不负责智能选位
|
||||
void enableTooltip(bool on) { tipEnabled = on; if (!on) tipVisible = false; }
|
||||
// 设置 Tooltip 延时(毫秒)
|
||||
// 开关 Tooltip;关闭时只修改内部启用/显示状态,不负责智能选位。
|
||||
// Tooltip 是按钮内部临时浮层,会并入托管重绘 coverage,但不会成为持久背景快照的一部分。
|
||||
void enableTooltip(bool on);
|
||||
// 查询 Tooltip 当前是否已经显示。
|
||||
bool isTooltipVisible() const { return tipVisible; }
|
||||
// 设置 Tooltip 延时(毫秒);负数会被夹紧为 0。
|
||||
void setTooltipDelay(int ms) { tipDelayMs = (ms < 0 ? 0 : ms); }
|
||||
// 设置 Tooltip 是否跟随鼠标
|
||||
void setTooltipFollowCursor(bool on) { tipFollowCursor = on; }
|
||||
// 设置 Tooltip 偏移量;当前仅在 setTooltipFollowCursor(true) 时参与定位
|
||||
// 设置 Tooltip 偏移量;当前仅在 setTooltipFollowCursor(true) 时参与定位。
|
||||
void setTooltipOffset(int dx, int dy) { tipOffsetX = dx; tipOffsetY = dy; }
|
||||
// 设置 Tooltip 的文字、背景和透明样式
|
||||
// 设置 Tooltip 的文字、背景和透明样式。
|
||||
void setTooltipStyle(COLORREF text, COLORREF bk, bool transparent);
|
||||
// 设置 NORMAL 模式下 Tooltip 文本
|
||||
// 设置 NORMAL 模式下 Tooltip 文本,并阻止自动跟随按钮文本。
|
||||
void setTooltipText(const std::string& s) { tipTextClick = s; tipUserOverride = true; }
|
||||
// 设置 TOGGLE 模式开/关两种状态下的 Tooltip 文本
|
||||
// 设置 TOGGLE 模式开/关两种状态下的 Tooltip 文本。
|
||||
void setTooltipTextsForToggle(const std::string& onText, const std::string& offText);
|
||||
private:
|
||||
// 初始化按钮内部状态、颜色和 Tooltip 默认样式
|
||||
@@ -185,12 +198,14 @@ private:
|
||||
bool isMouseInCircle(int mouseX, int mouseY, int x, int y, int radius);
|
||||
// 判断鼠标是否在椭圆按钮内
|
||||
bool isMouseInEllipse(int mouseX, int mouseY, int x, int y, int width, int height);
|
||||
//获取对话框类型
|
||||
// Button 不是模态对话框,固定返回 false。
|
||||
bool model() const override { return false; }
|
||||
// 按当前按钮宽度和语言特征裁剪文本
|
||||
void cutButtonText();
|
||||
// 统一隐藏 Tooltip 并恢复其背景快照
|
||||
// 统一隐藏 Tooltip 并恢复其背景快照。
|
||||
void hideTooltip();
|
||||
// 在最后一次记录的鼠标位置显示 Tooltip。
|
||||
void showTooltipAtLastMouse();
|
||||
// 根据当前 click 状态选择 Tooltip 文案
|
||||
void refreshTooltipTextForState();
|
||||
};
|
||||
@@ -23,8 +23,21 @@
|
||||
|
||||
class Canvas : public Control
|
||||
{
|
||||
protected:
|
||||
friend class TabControl;
|
||||
protected:
|
||||
std::vector<std::unique_ptr<Control>> controls;
|
||||
// 回调或遍历期间的增删清空先记录在队列中,防止 vector 变更使当前迭代器失效。
|
||||
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;
|
||||
|
||||
StellarX::ControlShape shape = StellarX::ControlShape::RECTANGLE; //容器形状
|
||||
StellarX::FillMode canvasFillMode = StellarX::FillMode::Solid; //容器填充模式
|
||||
@@ -34,13 +47,12 @@ protected:
|
||||
COLORREF canvasBorderClor = RGB(0, 0, 0); //边框颜色
|
||||
COLORREF canvasBkClor = RGB(255, 255, 255); //背景颜色
|
||||
|
||||
// 清除所有子控件
|
||||
void clearAllControls();
|
||||
public:
|
||||
// 默认构造:创建一个基础矩形容器
|
||||
Canvas();
|
||||
// 指定初始位置和尺寸构造容器
|
||||
Canvas(int x, int y, int width, int height);
|
||||
// 析构 Canvas;子控件由 unique_ptr 自动释放。
|
||||
~Canvas() {}
|
||||
|
||||
// 修改容器运行态 X,并同步重算全部受管理子控件的世界坐标
|
||||
@@ -52,46 +64,71 @@ public:
|
||||
// 修改容器运行态高度,并同步重算全部受管理子控件的世界坐标
|
||||
void setHeight(int height) override;
|
||||
|
||||
//绘制容器及其子控件
|
||||
// 绘制容器背景、边框和全部可见子控件。
|
||||
void draw() override;
|
||||
// 将鼠标和键盘事件按绘制层级分发给子控件。
|
||||
bool handleEvent(const ExMessage& msg) override;
|
||||
// 推进容器内可见子控件的时钟状态。
|
||||
void tick(ULONGLONG nowMs) override;
|
||||
// 递归清理子树中的 hover / tooltip / 临时按下态,不把 Canvas 自己升级为整块重绘源。
|
||||
bool clearTransientMouseState() override;
|
||||
// 添加一个由 Canvas 托管的子控件
|
||||
void addControl(std::unique_ptr<Control> control);
|
||||
//设置容器样式
|
||||
// 添加一个由 Canvas 托管的子控件并返回稳定句柄。
|
||||
// 若当前处于事件/绘制/tick 事务,unique_ptr 会先进入待提交队列,避免正在遍历的 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;
|
||||
// 设置容器几何形状。
|
||||
void setShape(StellarX::ControlShape shape);
|
||||
//设置容器填充模式
|
||||
// 设置容器填充模式。
|
||||
void setCanvasfillMode(StellarX::FillMode mode);
|
||||
//设置容器边框颜色
|
||||
// 设置容器边框颜色。
|
||||
void setBorderColor(COLORREF color);
|
||||
//设置填充颜色
|
||||
// 设置容器背景填充颜色。
|
||||
void setCanvasBkColor(COLORREF color);
|
||||
//设置线形
|
||||
// 设置容器边框线型。
|
||||
void setCanvasLineStyle(StellarX::LineStyle style);
|
||||
//设置线段宽度
|
||||
// 设置容器边框线宽。
|
||||
void setLinewidth(int width);
|
||||
//设置不可见后传递给子控件重写
|
||||
// 设置容器可见性,并向子控件递归同步可见状态。
|
||||
void setIsVisible(bool visible) override;
|
||||
// 设置容器脏标记,并按需递归标记子控件。
|
||||
void setDirty(bool dirty) override;
|
||||
// 窗口尺寸变化时作废容器和子树快照。
|
||||
void onWindowResize() override;
|
||||
// 托管模式下登记为 root;非托管模式下走局部或根级重绘
|
||||
// 托管模式下登记为 root;非托管模式下走局部或根级重绘。
|
||||
// 子控件视觉变化会提升到 Canvas root,保证背景快照和兄弟覆盖关系一致。
|
||||
void requestRepaint(Control* parent)override;
|
||||
// 判断 Canvas 或其受管理子树是否存在待提交的脏内容。
|
||||
bool hasManagedDirtySubtree() const override;
|
||||
// 获取 Canvas 托管重绘覆盖范围,包含需要随 root 一起重绘的子树区域。
|
||||
RECT getManagedRepaintCoverageRect() const override;
|
||||
// 获取 Canvas 持久绘制范围,用于判断兄弟背景快照是否需要失效。
|
||||
RECT getManagedRepaintPersistentCoverageRect() const override;
|
||||
// 判断当前 Canvas 是否可安全做局部提交
|
||||
bool canCommitManagedPartialRepaint() const override;
|
||||
// 托管收口阶段执行 Canvas 的真正重绘
|
||||
void commitManagedRepaint() override;
|
||||
// 获取直接子控件列表
|
||||
std::vector<std::unique_ptr<Control>>& getControls() { return controls; }
|
||||
protected:
|
||||
// 进入 Canvas 子树 mutation transaction;会向父容器/Window 传播事务深度。
|
||||
void beginMutationScope() override;
|
||||
// 离开 Canvas 子树 mutation transaction;最外层结束时提交子控件队列。
|
||||
void endMutationScope() override;
|
||||
// 提交 add/remove/clear 队列,并递归提交已存在子容器的 pending mutation。
|
||||
void flushPendingMutations() override;
|
||||
// 统一解算后,按当前运行态矩形把所有受管理子控件重新映射到新的世界坐标。
|
||||
void applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect) override;
|
||||
private:
|
||||
// 容器自己的几何变化后,需要统一刷新所有子控件的运行态矩形。
|
||||
void relayoutManagedChildren();
|
||||
//用来检查对话框是否模态,此控件不做实现
|
||||
// Canvas 不是模态对话框,固定返回 false。
|
||||
bool model() const override { return false; };
|
||||
};
|
||||
@@ -31,6 +31,7 @@
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <atomic>
|
||||
#include "CoreTypes.h"
|
||||
|
||||
class Window;
|
||||
@@ -40,6 +41,9 @@ class Control
|
||||
friend class Window;
|
||||
friend class Canvas;
|
||||
protected:
|
||||
inline static std::atomic<std::uint64_t> nextHandleValue{ 1 };
|
||||
// 句柄随控件构造生成,容器只通过句柄排队删除,避免遍历期间直接持有失效指针。
|
||||
StellarX::ControlHandle handle{ nextHandleValue.fetch_add(1, std::memory_order_relaxed) };
|
||||
std::string id; // 控件ID
|
||||
int localx, x, localy, y; // local* 为设计态父局部坐标,x/y 为运行态世界坐标
|
||||
int localWidth, width, localHeight, height; // local* 为设计态尺寸,width/height 为运行态尺寸
|
||||
@@ -75,32 +79,59 @@ protected:
|
||||
COLORREF currentBorderColor{}; // 边框颜色
|
||||
LINESTYLE currentLineStyle{}; // 保存当前线型
|
||||
|
||||
// 禁止拷贝构造,避免两个对象拥有同一个控件身份和背景快照。
|
||||
Control(const Control&) = delete;
|
||||
// 禁止拷贝赋值,保持控件所有权和句柄唯一性。
|
||||
Control& operator=(const Control&) = delete;
|
||||
// 禁止移动构造,避免容器外部移动后破坏父子关系和快照状态。
|
||||
Control(Control&&) = delete;
|
||||
// 禁止移动赋值,控件迁移统一通过 unique_ptr 所有权转移完成。
|
||||
Control& operator=(Control&&) = delete;
|
||||
|
||||
// 构造默认控件几何,供派生类无参构造使用。
|
||||
Control() : localx(0), x(0), localy(0), y(0), localWidth(100), width(100), height(100), localHeight(100) {}
|
||||
// 构造指定设计态和运行态矩形的基础控件。
|
||||
Control(int x, int y, int width, int height)
|
||||
: localx(x), x(x), localy(y), y(y), localWidth(width), width(width), height(height), localHeight(height) {
|
||||
}
|
||||
public:
|
||||
// 控件内部使用的 RAII 事务守卫。
|
||||
// 进入后子类可安全排队修改子树;析构时自动结束事务并在最外层触发提交。
|
||||
class MutationScope
|
||||
{
|
||||
// RAII 事务保证异常路径或提前 return 时也能成对结束嵌套变更范围。
|
||||
Control* owner = nullptr;
|
||||
public:
|
||||
// 进入 owner 的 mutation scope;owner 为空时为空操作。
|
||||
explicit MutationScope(Control* control) : owner(control) { if (owner) owner->beginMutationScope(); }
|
||||
// 禁止拷贝事务守卫,避免重复结束同一事务。
|
||||
MutationScope(const MutationScope&) = delete;
|
||||
// 禁止拷贝赋值事务守卫,事务所有权只能移动。
|
||||
MutationScope& operator=(const MutationScope&) = delete;
|
||||
// 移动事务守卫所有权,确保只有一个对象负责 endMutationScope。
|
||||
MutationScope(MutationScope&& other) noexcept : owner(other.owner) { other.owner = nullptr; }
|
||||
// 自动离开 owner 的 mutation scope。
|
||||
~MutationScope() { if (owner) owner->endMutationScope(); }
|
||||
};
|
||||
|
||||
// 析构控件并释放可能存在的背景快照。
|
||||
virtual ~Control()
|
||||
{
|
||||
discardBackground();
|
||||
}
|
||||
protected:
|
||||
// 向上请求重绘:普通路径交给父容器,托管路径则登记到 Window
|
||||
// 向上请求重绘:普通路径交给父容器,托管路径则登记到 Window。
|
||||
// 子类在事件中改变可见像素后应调用它,而不是直接假设自己能立即 draw。
|
||||
virtual void requestRepaint(Control* parent);
|
||||
// 根控件/无父时触发重绘
|
||||
// 根控件/无父时触发重绘;顶层控件通常通过 Window 托管收口统一处理。
|
||||
virtual void onRequestRepaintAsRoot();
|
||||
// 当前是否处于 Window 托管分发阶段;若为真,则不应立即画
|
||||
// 当前是否处于 Window 托管分发阶段;若为真,则不应立即画。
|
||||
// 该判断用于避免事件遍历中边改状态边回贴背景导致闪烁或快照错乱。
|
||||
bool shouldDeferManagedRepaint() const;
|
||||
protected:
|
||||
//保存背景快照
|
||||
// 保存指定区域的背景快照,供局部重绘前后恢复使用。
|
||||
virtual void saveBackground(int x, int y, int w, int h);
|
||||
// putimage 回屏
|
||||
// 将已保存的背景快照回贴到屏幕。
|
||||
virtual void restBackground();
|
||||
// 回贴旧背景并释放快照
|
||||
void discardBackground();
|
||||
@@ -109,19 +140,30 @@ public:
|
||||
void invalidateBackgroundSnapshot();
|
||||
//“纯作废快照 + 标脏”,不再在 resize 路径里回贴旧背景
|
||||
virtual void onWindowResize();
|
||||
// 获取位置和尺寸
|
||||
// 获取当前运行态 X 坐标。
|
||||
int getX() const { return x; }
|
||||
// 获取当前运行态 Y 坐标。
|
||||
int getY() const { return y; }
|
||||
// 获取当前运行态宽度。
|
||||
int getWidth() const { return width; }
|
||||
// 获取当前运行态高度。
|
||||
int getHeight() const { return height; }
|
||||
// 获取当前运行态右边界坐标。
|
||||
int getRight() const { return x + width; }
|
||||
// 获取当前运行态下边界坐标。
|
||||
int getBottom() const { return y + height; }
|
||||
|
||||
// 获取设计态父局部 X 坐标。
|
||||
int getLocalX() const { return localx; }
|
||||
// 获取设计态父局部 Y 坐标。
|
||||
int getLocalY() const { return localy; }
|
||||
// 获取设计态宽度。
|
||||
int getLocalWidth() const { return localWidth; }
|
||||
// 获取设计态高度。
|
||||
int getLocalHeight() const { return localHeight; }
|
||||
// 获取设计态父局部右边界。
|
||||
int getLocalRight() const { return localx + localWidth; }
|
||||
// 获取设计态父局部下边界。
|
||||
int getLocalBottom() const { return localy + localHeight; }
|
||||
|
||||
// 公开几何 setter:
|
||||
@@ -130,22 +172,30 @@ public:
|
||||
// - 不允许自动回写设计基线 local*
|
||||
// 若某个复合控件需要在 setter 后维护自己拥有的运行态子树一致性,
|
||||
// 由该控件在 override 中自行处理,但仍不得越权承担统一布局器职责。
|
||||
// 修改运行态 X 坐标,不自动回写设计态 localx。
|
||||
virtual void setX(int x);
|
||||
// 修改运行态 Y 坐标,不自动回写设计态 localy。
|
||||
virtual void setY(int y);
|
||||
// 修改运行态宽度,不自动回写设计态 localWidth。
|
||||
virtual void setWidth(int width);
|
||||
// 修改运行态高度,不自动回写设计态 localHeight。
|
||||
virtual void setHeight(int height);
|
||||
public:
|
||||
|
||||
// 绘制入口。实现应只在 dirty/show 条件满足时写屏,并在完成后清理 dirty。
|
||||
virtual void draw() = 0;
|
||||
virtual bool handleEvent(const ExMessage& msg) = 0;//返回true代表事件已消费
|
||||
// 事件入口。返回 true 表示事件已消费;若事件造成视觉变化,应设置 eventVisualChanged 并请求重绘。
|
||||
virtual bool handleEvent(const ExMessage& msg) = 0;
|
||||
// 轻量清理接口:仅用于“没有机会收到本次 WM_MOUSEMOVE”的兄弟分支,
|
||||
// 清掉 hover / tooltip / 临时按下态等鼠标瞬时状态。
|
||||
// 该接口不是事件分发入口,不参与新的命中判断,也不允许回写设计基线。
|
||||
virtual bool clearTransientMouseState();
|
||||
//设置是否显示
|
||||
// 由 Window 驱动的单调时钟更新;动画、延时提示等不得依赖额外鼠标消息。
|
||||
virtual void tick(ULONGLONG) {}
|
||||
// 设置控件是否显示;隐藏时派生类可同步清理快照和子控件状态。
|
||||
virtual void setIsVisible(bool show);
|
||||
//设置父容器指针
|
||||
void setParent(Control* parent) { this->parent = parent; }
|
||||
// 设置父容器指针;通常由容器接管子控件时调用。
|
||||
void setParent(Control* newParent) { parent = newParent; }
|
||||
//设置宿主窗口(通常仅由顶层 Window/对话框注入)
|
||||
virtual void setHostWindow(Window* host) { this->hostWindow = host; }
|
||||
Window* getHostWindow() const; // 获取宿主 Window;子控件可沿 parent 向上回溯
|
||||
@@ -157,23 +207,30 @@ public:
|
||||
// 获取会影响后续控件背景快照的“持久绘制范围”。
|
||||
// Tooltip 等临时浮层不应进入该范围,避免上层兄弟补画时把临时浮层抓进背景快照。
|
||||
virtual RECT getManagedRepaintPersistentCoverageRect() const;
|
||||
Control* getManagedRepaintRoot(); // 找到本控件对应的托管重绘 root
|
||||
Control* getManagedRepaintDirectBranch(Control* root); // 找到“root 下面承接本次脏变化的直接子分支”
|
||||
// 找到本控件对应的托管重绘 root;通常是直接挂在 Window 下的控件或 Dialog。
|
||||
Control* getManagedRepaintRoot();
|
||||
// 找到“root 下面承接本次脏变化的直接子分支”,用于局部提交时缩小重绘范围。
|
||||
Control* getManagedRepaintDirectBranch(Control* root);
|
||||
bool hasValidBackgroundSnapshot() const { return hasSnap && saveBkImage != nullptr; } // 当前是否持有可用于局部恢复的快照
|
||||
virtual bool hasManagedDirtySubtree() const; // 当前控件或其受管理子树中是否存在待提交的脏内容
|
||||
virtual bool canCommitManagedPartialRepaint() const; // 当前 root 是否可安全做“局部提交”而非整 root 重画
|
||||
virtual void commitManagedRepaint(); // 托管收口阶段真正执行绘制的入口
|
||||
//设置是否重绘
|
||||
virtual void setDirty(bool dirty) { this->dirty = dirty; }
|
||||
//检查控件是否可见
|
||||
// 当前控件或其受管理子树中是否存在待提交的脏内容;容器需要递归检查子树。
|
||||
virtual bool hasManagedDirtySubtree() const;
|
||||
// 当前 root 是否可安全做“局部提交”而非整 root 重画;复杂容器可返回 false 升级为整 root 重绘。
|
||||
virtual bool canCommitManagedPartialRepaint() const;
|
||||
// 托管收口阶段真正执行绘制的入口;只应由 Window 的 flushManagedRepaint 调用。
|
||||
virtual void commitManagedRepaint();
|
||||
// 设置控件脏标记;true 表示下一轮需要重新绘制。
|
||||
virtual void setDirty(bool isDirty) { dirty = isDirty; }
|
||||
// 检查控件当前是否可见。
|
||||
bool IsVisible() const { return show; };
|
||||
//获取控件id
|
||||
// 获取控件类型/调试标识。
|
||||
std::string getId() const { return id; }
|
||||
//检查是否为脏
|
||||
// 返回稳定句柄而非对象地址;调用方不得据此推断控件当前仍在容器中。
|
||||
StellarX::ControlHandle getHandle() const noexcept { return handle; }
|
||||
// 检查控件当前是否处于待重绘状态。
|
||||
bool isDirty() { return dirty; }
|
||||
//获取控件最近一次事件处理是否引发了视觉变化
|
||||
bool didEventAffectVisual() const { return eventVisualChanged; }
|
||||
//用来检查对话框是否模态,其他控件不用实现
|
||||
// 判断控件是否为模态对话框;普通控件应返回 false。
|
||||
virtual bool model()const = 0;
|
||||
//布局
|
||||
// 设置旧版布局模式(兼容入口)
|
||||
@@ -221,6 +278,14 @@ public:
|
||||
// - 仅用于少数需要把当前结果固化为新设计基线的场景
|
||||
void commitCurrentGeometryAsDesignRect();
|
||||
protected:
|
||||
// 容器和窗口在事件、绘制、tick 期间进入事务;具体容器在最外层结束时提交队列。
|
||||
MutationScope enterMutationScope() { return MutationScope(this); }
|
||||
// 进入子树结构变更事务。默认向父容器或宿主 Window 传播,使整棵树共享同一提交边界。
|
||||
virtual void beginMutationScope();
|
||||
// 离开子树结构变更事务。最外层结束后才允许容器提交 pending mutation。
|
||||
virtual void endMutationScope();
|
||||
// 提交子类容器自己的 pending mutation;普通叶子控件保持空实现。
|
||||
virtual void flushPendingMutations() {}
|
||||
// 第 1 层:根据父设计尺寸、父当前尺寸和本控件设计矩形,解算出当前运行态局部矩形。
|
||||
StellarX::ResolvedLayoutRect resolveLayoutRect(int parentDesignW, int parentDesignH,
|
||||
int parentWorldX, int parentWorldY, int parentCurrentW, int parentCurrentH) const;
|
||||
@@ -228,8 +293,12 @@ protected:
|
||||
virtual void applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect);
|
||||
// 最底层运行态赋值入口:仅修改运行态矩形,不回写 local*。
|
||||
void applyRuntimeRectDirect(int worldX, int worldY, int width, int height);
|
||||
// 保存当前 EasyX 绘图状态,避免控件绘制污染全局状态。
|
||||
void saveStyle();
|
||||
// 恢复 saveStyle() 保存的 EasyX 绘图状态。
|
||||
void restoreStyle();
|
||||
// 清空本轮事件视觉变化标记。
|
||||
void resetEventVisualChanged() { eventVisualChanged = false; }
|
||||
// 标记本轮事件是否引发视觉变化。
|
||||
void markEventVisualChanged(bool changed = true) { eventVisualChanged = changed; }
|
||||
};
|
||||
@@ -11,6 +11,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "easyx.h"
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
/**
|
||||
* @命名空间: StellarX
|
||||
@@ -27,6 +29,41 @@
|
||||
*/
|
||||
namespace StellarX
|
||||
{
|
||||
/**
|
||||
* @结构体名称: ControlHandle
|
||||
* @功能描述: 表示控件在当前进程内的稳定身份句柄
|
||||
*
|
||||
* @详细说明:
|
||||
* 控件由 Window、Canvas、TabControl 等容器持有所有权,外部调用方不应长期保存
|
||||
* 控件对象地址。ControlHandle 用一个不暴露对象地址的整数值表示控件身份,
|
||||
* 用于后续 removeControl、findControl、getControlHandles 等安全查询和删除操作。
|
||||
*
|
||||
* 句柄值为 0 时表示无效句柄;非 0 值由控件构造时单调分配,只保证当前进程
|
||||
* 本次运行期间的身份稳定,不保证跨进程、跨运行或序列化后仍然有效。
|
||||
*
|
||||
* @成员说明:
|
||||
* std::uint64_t value = 0; - 句柄原始值,0 表示无效句柄。
|
||||
*
|
||||
* @使用示例:
|
||||
* StellarX::ControlHandle handle = wnd.addControl(std::move(control));
|
||||
* if (handle) wnd.removeControl(handle);
|
||||
*
|
||||
* @备注:
|
||||
* 控件被移除后,旧句柄仍可安全用于比较或查询,但 findControl 可能返回 nullptr;
|
||||
* 调用方不得把句柄视为对象生命周期仍然存在的证明。
|
||||
*/
|
||||
struct ControlHandle
|
||||
{
|
||||
std::uint64_t value = 0;
|
||||
|
||||
// 判断句柄是否有效;value 为 0 时返回 false。
|
||||
explicit operator bool() const noexcept { return value != 0; }
|
||||
// 比较两个句柄是否指向同一控件身份。
|
||||
bool operator==(const ControlHandle& other) const noexcept { return value == other.value; }
|
||||
// 比较两个句柄是否指向不同控件身份。
|
||||
bool operator!=(const ControlHandle& other) const noexcept { return value != other.value; }
|
||||
};
|
||||
|
||||
/**
|
||||
* @枚举类名称: FillStyle
|
||||
* @功能描述: 用来定义控件填充图案的枚举类
|
||||
@@ -131,7 +168,7 @@ namespace StellarX
|
||||
* @成员说明:
|
||||
* int nHeight = 0; - 字体高度
|
||||
* int nWidth = 0; - 字体宽度 如果为0则自适应
|
||||
* LPCTSTR lpszFace = "宋体"; - 字体名称
|
||||
* std::string faceName = "微软雅黑"; - 字体名称
|
||||
* COLORREF color = RGB(0, 0, 0); - 字体颜色
|
||||
* int nEscapement = 0; - 字符串旋转角度
|
||||
* int nOrientation = 0; - 字符旋转角度
|
||||
@@ -144,7 +181,8 @@ namespace StellarX
|
||||
{
|
||||
int nHeight = 0; //- 字体高度
|
||||
int nWidth = 0; //- 字体宽度 如果为0则自适应
|
||||
LPCTSTR lpszFace = "微软雅黑"; //- 字体名称
|
||||
// 由对象拥有字体名,避免从临时字符串或外部缓冲区保存悬空指针。
|
||||
std::string faceName = "微软雅黑"; //- 字体名称
|
||||
COLORREF color = RGB(0, 0, 0); //- 字体颜色
|
||||
int nEscapement = 0; //- 字符串旋转角度
|
||||
int nOrientation = 0; //- 字符旋转角度
|
||||
@@ -153,8 +191,10 @@ namespace StellarX
|
||||
bool bUnderline = false; //- 是否下划线
|
||||
bool bStrikeOut = false; //- 是否删除线
|
||||
|
||||
bool operator!=(const ControlText& text);
|
||||
ControlText& operator=(const ControlText& text);
|
||||
// 比较两份字体样式是否完全一致。
|
||||
bool operator==(const ControlText& text) const;
|
||||
// 比较两份字体样式是否存在任意差异。
|
||||
bool operator!=(const ControlText& text) const { return !(*this == text); }
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -252,7 +292,25 @@ namespace StellarX
|
||||
int ROUND_RECTANGLEheight = 20; //构成圆角矩形的圆角的椭圆的高度。
|
||||
};
|
||||
|
||||
// 消息框类型
|
||||
/**
|
||||
* @枚举名称: MessageBoxType
|
||||
* @功能描述: 定义标准消息框按钮组合
|
||||
*
|
||||
* @详细说明:
|
||||
* MessageBoxType 决定 Dialog / MessageBox 工厂创建哪些功能按钮,
|
||||
* 例如只有确定按钮、确定取消按钮、是否按钮等。
|
||||
*
|
||||
* @取值说明:
|
||||
* OK - 只有确定按钮
|
||||
* OKCancel - 确定和取消按钮
|
||||
* YesNo - 是和否按钮
|
||||
* YesNoCancel - 是、否和取消按钮
|
||||
* RetryCancel - 重试和取消按钮
|
||||
* AbortRetryIgnore - 中止、重试和忽略按钮
|
||||
*
|
||||
* @使用示例:
|
||||
* StellarX::MessageBoxType type = StellarX::MessageBoxType::YesNo;
|
||||
*/
|
||||
enum class MessageBoxType
|
||||
{
|
||||
OK, // 只有确定按钮
|
||||
@@ -263,7 +321,27 @@ namespace StellarX
|
||||
AbortRetryIgnore, // 中止、重试和忽略按钮
|
||||
};
|
||||
|
||||
// 消息框返回值
|
||||
/**
|
||||
* @枚举名称: MessageBoxResult
|
||||
* @功能描述: 定义消息框关闭后的用户选择结果
|
||||
*
|
||||
* @详细说明:
|
||||
* 模态消息框通过 showModal 返回该枚举;非模态消息框通过 showAsync 的回调返回。
|
||||
* 当前取值与 Win32 常见 MessageBox 返回码保持一致,便于迁移和日志排查。
|
||||
*
|
||||
* @取值说明:
|
||||
* OK - 用户点击确定
|
||||
* Cancel - 用户点击取消或关闭对话框
|
||||
* Yes - 用户点击是
|
||||
* No - 用户点击否
|
||||
* Abort - 用户点击中止
|
||||
* Retry - 用户点击重试
|
||||
* Ignore - 用户点击忽略
|
||||
*
|
||||
* @使用示例:
|
||||
* StellarX::MessageBoxResult result =
|
||||
* StellarX::MessageBox::showModal(wnd, "继续执行?", "确认", StellarX::MessageBoxType::YesNo);
|
||||
*/
|
||||
enum class MessageBoxResult
|
||||
{
|
||||
OK = 1, // 确定按钮
|
||||
@@ -428,13 +506,22 @@ namespace StellarX
|
||||
};
|
||||
|
||||
/*******************************************************************************
|
||||
* @结构体: AxisLayoutSpec
|
||||
* @结构体名称: AxisLayoutSpec
|
||||
* @摘要: 描述单个坐标轴上的锚点与解算策略
|
||||
* @详细说明:
|
||||
* 一个控件完整布局由“水平轴 + 垂直轴”两份 AxisLayoutSpec 组成。
|
||||
* anchorStart / anchorEnd 只表达当前轴是否锚定起止边;
|
||||
* sizePolicy 决定该轴是否允许拉伸;
|
||||
* alignPolicy 则用于固定尺寸时的位置求解。
|
||||
*
|
||||
* @成员说明:
|
||||
* bool anchorStart - 是否锚定当前轴起边(水平轴为左边,垂直轴为上边)。
|
||||
* bool anchorEnd - 是否锚定当前轴终边(水平轴为右边,垂直轴为下边)。
|
||||
* AxisSizePolicy sizePolicy - 当前轴尺寸策略,决定是否随父容器变化而拉伸。
|
||||
* AxisAlignPolicy alignPolicy - 固定尺寸时的位置策略。
|
||||
*
|
||||
* @备注:
|
||||
* anchorStart / anchorEnd 不直接决定尺寸,尺寸是否变化以 sizePolicy 为准。
|
||||
******************************************************************************/
|
||||
struct AxisLayoutSpec
|
||||
{
|
||||
@@ -445,8 +532,16 @@ namespace StellarX
|
||||
};
|
||||
|
||||
/*******************************************************************************
|
||||
* @结构体: LayoutSpec
|
||||
* @结构体名称: LayoutSpec
|
||||
* @摘要: 控件完整布局规格,由水平轴和垂直轴两部分组成
|
||||
* @详细说明:
|
||||
* LayoutSpec 是控件对统一布局器的完整声明。水平轴和垂直轴分别独立描述
|
||||
* 锚点、尺寸策略和固定尺寸时的位置策略,Window / Canvas 在 resize 时据此
|
||||
* 解算运行态矩形。
|
||||
*
|
||||
* @成员说明:
|
||||
* AxisLayoutSpec horizontal - 水平轴布局规格。
|
||||
* AxisLayoutSpec vertical - 垂直轴布局规格。
|
||||
******************************************************************************/
|
||||
struct LayoutSpec
|
||||
{
|
||||
@@ -455,12 +550,20 @@ namespace StellarX
|
||||
};
|
||||
|
||||
/*******************************************************************************
|
||||
* @结构体: LayoutCapability
|
||||
* @结构体名称: LayoutCapability
|
||||
* @摘要: 控件能力边界,声明控件在哪些轴上允许 Stretch
|
||||
* @详细说明:
|
||||
* 规则表负责定义“某种组合如何解算”,LayoutCapability 负责声明
|
||||
* “该控件是否允许采用这种组合”。例如 Table 当前阶段仅允许 X 轴拉伸,
|
||||
* 因此 allowStretchY 会被显式关闭。
|
||||
*
|
||||
* @成员说明:
|
||||
* bool allowStretchX - 是否允许该控件在水平轴使用 Stretch 尺寸策略。
|
||||
* bool allowStretchY - 是否允许该控件在垂直轴使用 Stretch 尺寸策略。
|
||||
*
|
||||
* @备注:
|
||||
* 能力边界用于保护控件自身实现约束;即使调用方设置了 Stretch,
|
||||
* 不被允许的轴也应被布局层拒绝或降级处理。
|
||||
******************************************************************************/
|
||||
struct LayoutCapability
|
||||
{
|
||||
@@ -469,12 +572,20 @@ namespace StellarX
|
||||
};
|
||||
|
||||
/*******************************************************************************
|
||||
* @结构体: ResolvedLayoutRect
|
||||
* @结构体名称: ResolvedLayoutRect
|
||||
* @摘要: 统一布局解算后的运行态矩形结果
|
||||
* @详细说明:
|
||||
* localX/localY 表示解算完成后的“父局部坐标”;
|
||||
* worldX/worldY 表示映射到窗口世界坐标后的最终绘制位置;
|
||||
* width/height 为运行态尺寸。
|
||||
*
|
||||
* @成员说明:
|
||||
* int localX - 解算后的父局部 X 坐标。
|
||||
* int localY - 解算后的父局部 Y 坐标。
|
||||
* int width - 解算后的运行态宽度。
|
||||
* int height - 解算后的运行态高度。
|
||||
* int worldX - 映射到窗口坐标系后的 X 坐标。
|
||||
* int worldY - 映射到窗口坐标系后的 Y 坐标。
|
||||
******************************************************************************/
|
||||
struct ResolvedLayoutRect
|
||||
{
|
||||
@@ -65,11 +65,12 @@ class Dialog : public Canvas
|
||||
bool pendingCleanup = false; //延迟清理
|
||||
public:
|
||||
StellarX::ControlText textStyle; // 字体样式
|
||||
// 在事件安全点执行延迟清理;用于非模态关闭后统一回收
|
||||
// 在事件安全点执行延迟清理;用于非模态关闭后统一回收。
|
||||
// Close() 只隐藏并标记待清理,真正释放内部按钮和快照要等到这里执行。
|
||||
void performDelayedCleanup();
|
||||
// 非模态工厂调用时用于回传结果
|
||||
// 非模态工厂调用时用于回传结果;回调可能修改宿主控件树,因此 Close() 会在 mutation scope 内触发它。
|
||||
std::function<void(StellarX::MessageBoxResult)> resultCallback;
|
||||
// 设置非模态结果回调
|
||||
// 设置非模态结果回调;仅在非模态 Dialog 关闭时异步调用。
|
||||
void SetResultCallback(std::function<void(StellarX::MessageBoxResult)> cb);
|
||||
// 获取标题文本,用于非模态去重
|
||||
std::string GetCaption() const;
|
||||
@@ -77,59 +78,64 @@ public:
|
||||
std::string GetText() const;
|
||||
|
||||
public:
|
||||
// 构造对话框;modal=false 时按非模态浮层工作
|
||||
// 构造对话框;modal=false 时按非模态浮层工作,由 Window 接管生命周期和层级。
|
||||
Dialog(Window& hWnd, std::string text, std::string message = "对话框", StellarX::MessageBoxType type = StellarX::MessageBoxType::OK, bool modal = true);
|
||||
// 析构对话框并释放内部控件资源。
|
||||
~Dialog();
|
||||
//绘制对话框
|
||||
// 绘制对话框背景、标题、正文和功能按钮。
|
||||
void draw() override;
|
||||
//事件处理
|
||||
// 处理关闭按钮和功能按钮事件。
|
||||
bool handleEvent(const ExMessage& msg) override;
|
||||
// 设置标题
|
||||
// 设置标题并标记布局需要重建。
|
||||
void SetTitle(const std::string& title);
|
||||
// 设置消息内容
|
||||
// 设置消息内容并标记文本换行和布局需要重建。
|
||||
void SetMessage(const std::string& message);
|
||||
// 设置对话框类型
|
||||
// 设置对话框类型并重建功能按钮组合。
|
||||
void SetType(StellarX::MessageBoxType type);
|
||||
// 设置模态属性
|
||||
// 设置模态属性,决定 Show() 使用阻塞循环还是交给 Window 主循环。
|
||||
void SetModal(bool modal);
|
||||
// 设置对话框结果
|
||||
// 设置对话框结果,通常由功能按钮回调写入。
|
||||
void SetResult(StellarX::MessageBoxResult result);
|
||||
// 获取对话框结果
|
||||
StellarX::MessageBoxResult GetResult() const;
|
||||
|
||||
// 返回当前对话框是否为模态
|
||||
// 返回当前对话框是否为模态;Window 用它区分阻塞式对话框和托管浮层。
|
||||
bool model() const override;
|
||||
|
||||
// 显示对话框
|
||||
// 显示对话框。模态对话框会进入自己的消息循环;非模态对话框只标记可见,由 Window 主循环驱动。
|
||||
void Show();
|
||||
// 关闭对话框
|
||||
// 关闭对话框。
|
||||
// 该调用只设置隐藏、结果和待清理标志,并请求整场景重绘;对象释放延迟到 performDelayedCleanup()。
|
||||
void Close();
|
||||
// 宿主窗口变化时仅重新居中,不拉伸 Dialog 自身
|
||||
void recenterInHostWindow();
|
||||
|
||||
private:
|
||||
// 初始化按钮
|
||||
// 根据当前 MessageBoxType 初始化功能按钮。
|
||||
void initButtons();
|
||||
// 初始化关闭按钮
|
||||
// 初始化标题栏关闭按钮。
|
||||
void initCloseButton();
|
||||
// 按行分割消息内容
|
||||
// 按当前消息内容和宽度约束分割显示行。
|
||||
void splitMessageLines();
|
||||
// 获取文本大小
|
||||
// 计算当前标题和正文所需的文本尺寸。
|
||||
void getTextSize();
|
||||
// 标记需要重新布局并按需清空旧控件
|
||||
void invalidateLayout(bool clearChildren);
|
||||
//初始化对话框尺寸
|
||||
// 根据文本和按钮数量初始化对话框尺寸。
|
||||
void initDialogSize();
|
||||
// 依据当前 Dialog 的 x/y/width/height 重新创建标题和按钮
|
||||
void rebuildChrome();
|
||||
// 禁止走普通 Canvas 添加路径;Dialog 内部控件由自身 chrome 重建逻辑统一维护
|
||||
void addControl(std::unique_ptr<Control> control);
|
||||
bool canCommitManagedPartialRepaint() const override; // 判断当前 Dialog 是否可安全做局部提交
|
||||
void commitManagedRepaint() override; // 托管收口阶段执行 Dialog 的真正重绘
|
||||
// 判断当前 Dialog 是否可安全做局部提交。
|
||||
bool canCommitManagedPartialRepaint() const override;
|
||||
// 托管收口阶段执行 Dialog 的真正重绘。
|
||||
void commitManagedRepaint() override;
|
||||
|
||||
// 清除所有控件
|
||||
// 清除 Dialog 内部所有 chrome 控件。
|
||||
void clearControls();
|
||||
// 创建标准对话框功能按钮
|
||||
// 创建标准对话框功能按钮。
|
||||
std::unique_ptr<Button> createDialogButton(int x, int y, const std::string& text);
|
||||
void requestRepaint(Control* parent) override; // 托管模式下登记为 Dialog root;非托管模式下立即更新内部按钮
|
||||
// 请求宿主窗口重绘 Dialog root;非托管模式下立即更新内部按钮。
|
||||
void requestRepaint(Control* parent) override;
|
||||
};
|
||||
@@ -28,9 +28,9 @@ class Label : public Control
|
||||
StellarX::ControlText lastMeasuredStyle; // 最近一次参与尺寸测量的字体样式
|
||||
bool contentMeasureValid = false; // 当前内容尺寸缓存是否有效
|
||||
|
||||
//标签事件处理(标签无事件)不实现具体代码
|
||||
bool handleEvent(const ExMessage& msg) override { return false; }
|
||||
//用来检查对话框是否模态,此控件不做实现
|
||||
// Label 不处理输入事件,固定返回 false。
|
||||
bool handleEvent(const ExMessage&) override { return false; }
|
||||
// Label 不是模态对话框,固定返回 false。
|
||||
bool model() const override { return false; };
|
||||
// 根据当前文本和字体样式刷新运行态尺寸。
|
||||
// 这是 Label 的内容驱动几何入口:
|
||||
@@ -51,7 +51,7 @@ public:
|
||||
|
||||
// 绘制标签文本
|
||||
void draw() override;
|
||||
//用于“隐藏提示框”时调用(还原并释放快照)
|
||||
// 隐藏 Label 并恢复背景;主要供 Button Tooltip 关闭时复用。
|
||||
void hide();
|
||||
// Label 的内容尺寸由文本和字体样式共同决定。
|
||||
// 因此只要进入 dirty 状态,就在绘制前先刷新一次运行态尺寸,
|
||||
@@ -61,7 +61,8 @@ public:
|
||||
void setTextdisap(bool key);
|
||||
// 设置标签背景颜色
|
||||
void setTextBkColor(COLORREF color);
|
||||
// 设置标签文本,并触发内容驱动尺寸刷新
|
||||
// 设置标签文本,并触发内容驱动尺寸刷新。
|
||||
// Label 的布局系统只负责位置;运行态宽高仍由文本和 textStyle 测量结果决定。
|
||||
void setText(std::string text);
|
||||
protected:
|
||||
// 统一布局器只负责安置 Label 的运行态位置;
|
||||
@@ -27,15 +27,16 @@ namespace StellarX
|
||||
class MessageBox
|
||||
{
|
||||
public:
|
||||
// 模态:阻塞直到关闭,返回结果
|
||||
// 直接创建并显示模态消息框,直到用户关闭后返回结果
|
||||
// 模态:直接创建并显示对话框,阻塞到用户关闭后返回结果。
|
||||
// 调用期间会运行 Dialog 自己的模态事件循环。
|
||||
static MessageBoxResult showModal(Window& wnd,
|
||||
const std::string& text,
|
||||
const std::string& caption = "提示",
|
||||
MessageBoxType type = MessageBoxType::OK);
|
||||
|
||||
// 非模态:立即返回,通过回调异步获取结果
|
||||
// 创建并显示非模态消息框,关闭时通过回调返回结果
|
||||
// 非模态:创建后立即返回,由 Window 托管生命周期和绘制层级。
|
||||
// 若同一窗口中已存在相同 caption/text 的可见非模态对话框,则不会重复创建。
|
||||
// 用户关闭时通过 onResult 异步返回结果;回调允许排队修改宿主控件树。
|
||||
static void showAsync(Window& wnd,
|
||||
const std::string& text,
|
||||
const std::string& caption = "提示",
|
||||
@@ -141,6 +141,7 @@ namespace StellarX
|
||||
// 绑定一个输出流引用(常见用法:std::cout)
|
||||
explicit ConsoleSink(std::ostream& os) : out(os) {}
|
||||
|
||||
// 返回控制台 sink 的固定名称。
|
||||
const char* name() const override { return "console"; }
|
||||
|
||||
// 写入一行(不自动追加换行,换行由上层统一拼接)
|
||||
@@ -158,8 +159,10 @@ namespace StellarX
|
||||
class FileSink : public ILogSink
|
||||
{
|
||||
public:
|
||||
// 构造一个尚未打开文件的文件 sink。
|
||||
FileSink() = default;
|
||||
|
||||
// 返回文件 sink 的固定名称。
|
||||
const char* name() const override { return "file"; }
|
||||
|
||||
// 打开文件
|
||||
@@ -192,6 +195,8 @@ namespace StellarX
|
||||
std::string filePath; // 当前文件路径
|
||||
bool appendMode = true; // 是否追加模式(用于 reopen)
|
||||
std::size_t rotateBytes = 0; // 滚动阈值
|
||||
// 时间戳精度不足时用递增序号补足文件名唯一性,避免连续滚动互相覆盖。
|
||||
std::uint64_t rotateSequence = 0;
|
||||
};
|
||||
|
||||
/* ========================= 日志中心 SxLogger ========================= */
|
||||
@@ -264,8 +269,8 @@ namespace StellarX
|
||||
// 获取配置副本(避免外部直接改内部 cfg)
|
||||
SxLogConfig getConfigCopy() const;
|
||||
|
||||
// 批量设置配置(整体替换)
|
||||
void setConfig(const SxLogConfig& cfg);
|
||||
// 批量设置配置(整体替换);新文件 sink 打开成功后才替换旧配置。
|
||||
bool setConfig(const SxLogConfig& cfg);
|
||||
|
||||
// 工具:把级别转为字符串(用于前缀)
|
||||
static const char* levelToString(SxLogLevel level);
|
||||
@@ -274,6 +279,7 @@ namespace StellarX
|
||||
static std::string makeTimestampLocal();
|
||||
|
||||
private:
|
||||
// 构造日志单例的初始配置和默认 sink。
|
||||
SxLogger();
|
||||
|
||||
// 判断 tag 是否允许输出(根据 Tag 过滤模式与 tagList)
|
||||
@@ -300,6 +306,7 @@ namespace StellarX
|
||||
// 说明:
|
||||
// - 只做“选择 zhCN 或 enUS”,不做编码转换
|
||||
// - 输出显示是否正常由终端环境决定
|
||||
// 根据当前日志语言选择中文或英文文本。
|
||||
inline const char* SxT(const char* zhCN, const char* enUS)
|
||||
{
|
||||
return (SxLogger::Get().getLanguage() == SxLogLanguage::ZhCN) ? zhCN : enUS;
|
||||
@@ -309,6 +316,7 @@ namespace StellarX
|
||||
// 说明:
|
||||
// - C++20 的 u8"xxx" 是 char8_t*,为了兼容调用端,这里提供重载
|
||||
// - reinterpret_cast 只是改指针类型,不做 UTF-8 -> GBK 转码
|
||||
// 兼容 C++20 char8_t 字符串字面量的双语选择入口。
|
||||
inline const char* SxT(const char8_t* zhCN, const char* enUS)
|
||||
{
|
||||
return (SxLogger::Get().getLanguage() == SxLogLanguage::ZhCN)
|
||||
@@ -337,6 +345,7 @@ namespace StellarX
|
||||
|
||||
// 拼接内容(流式写法)
|
||||
template<typename T>
|
||||
// 向当前日志行追加一个可流式输出的值。
|
||||
SxLogLine& operator<<(const T& v)
|
||||
{
|
||||
ss << v;
|
||||
@@ -31,6 +31,8 @@ class TabControl :public Canvas
|
||||
int defaultActivation = -1; //默认激活页签索引
|
||||
StellarX::TabPlacement tabPlacement = StellarX::TabPlacement::Top; //页签排列方式
|
||||
std::vector<std::pair<std::unique_ptr<Button>, std::unique_ptr<Canvas>>> controls; //页签/页列表
|
||||
// addTab 在事务期间暂存完整的按钮/页面对,提交时保持两者的索引一致。
|
||||
std::vector<std::pair<std::unique_ptr<Button>, std::unique_ptr<Canvas>>> pendingTabs;
|
||||
|
||||
private:
|
||||
using Canvas::addControl; // 禁止外部误用
|
||||
@@ -38,56 +40,79 @@ private:
|
||||
private:
|
||||
// 初始化页签按钮位置和尺寸
|
||||
inline void initTabBar();
|
||||
// 初始化页面 Canvas 的位置和尺寸
|
||||
inline void initTabPage();
|
||||
// 统一刷新 TabControl 当前运行态下的页签栏和页面区布局。
|
||||
void refreshRuntimeLayout();
|
||||
// 立即加入一组已校验的页签按钮和页面 Canvas,并建立内部回调联动。
|
||||
size_t addTabDirect(std::unique_ptr<Button> button, std::unique_ptr<Canvas> page);
|
||||
public:
|
||||
// 默认构造:创建一个空 TabControl
|
||||
TabControl();
|
||||
// 指定初始位置和尺寸构造 TabControl
|
||||
TabControl(int x, int y, int width, int height);
|
||||
// 析构 TabControl;页签按钮和页面由 unique_ptr 自动释放。
|
||||
~TabControl();
|
||||
|
||||
//重写位置设置以适应页签和页面布局
|
||||
// 修改 TabControl 运行态 X,并刷新页签栏和页面区布局。
|
||||
void setX(int x)override;
|
||||
// 修改 TabControl 运行态 Y,并刷新页签栏和页面区布局。
|
||||
void setY(int y)override;
|
||||
// 修改 TabControl 运行态宽度,并刷新页签栏和页面区布局。
|
||||
void setWidth(int width) override;
|
||||
// 修改 TabControl 运行态高度,并刷新页签栏和页面区布局。
|
||||
void setHeight(int height) override;
|
||||
|
||||
// 绘制页签栏和当前可见页面。
|
||||
void draw() override;
|
||||
// 处理页签按钮和当前页面的事件分发。
|
||||
bool handleEvent(const ExMessage& msg) override;
|
||||
// 只推进当前页按钮和页面中的时钟状态,避免不可见页签产生无效工作。
|
||||
void tick(ULONGLONG nowMs) override;
|
||||
// 只转发清理页签按钮和当前可见页面中的鼠标临时状态,不升级为整块 TabControl 重绘。
|
||||
bool clearTransientMouseState() override;
|
||||
|
||||
// 添加一个“页签按钮 + 页面 Canvas”组合
|
||||
void add(std::pair<std::unique_ptr<Button>, std::unique_ptr<Canvas>>&& control);
|
||||
// 按页签文本把控件添加到对应页面
|
||||
void add(std::string tabText, std::unique_ptr<Control> control);
|
||||
//设置页签位置
|
||||
// 添加一个“页签按钮 + 页面 Canvas”组合并返回页签索引。
|
||||
// button/page 为空时返回 size_t(-1)。事务期间返回包含 pendingTabs 的逻辑索引,
|
||||
// 因此调用方可以立即把控件追加到刚创建但尚未提交的页面中。
|
||||
size_t addTab(std::unique_ptr<Button> button, std::unique_ptr<Canvas> page);
|
||||
// 向指定页追加控件。成功时返回空指针;失败时把控件所有权原样返还调用方。
|
||||
// pageIndex 可指向已提交页面,也可指向 addTab 在同一事务中返回的 pending 逻辑索引。
|
||||
std::unique_ptr<Control> tryAddToPage(size_t pageIndex, std::unique_ptr<Control> control);
|
||||
// 按页签文本追加控件;若存在重复标题,只匹配第一个命中的页签。
|
||||
std::unique_ptr<Control> tryAddToPage(const std::string& tabText, std::unique_ptr<Control> control);
|
||||
// 设置页签位置,并重新计算页签栏和页面区布局。
|
||||
void setTabPlacement(StellarX::TabPlacement placement);
|
||||
//设置页签栏高度 两侧排列时为宽度
|
||||
// 设置页签栏高度;当页签在左右两侧时表示页签栏宽度。
|
||||
void setTabBarHeight(int height);
|
||||
//设置不可见后传递给子控件重写
|
||||
// 设置 TabControl 可见性,并同步页签按钮和页面可见状态。
|
||||
void setIsVisible(bool visible) override;
|
||||
// 窗口尺寸变化时作废页签按钮和页面快照。
|
||||
void onWindowResize() override;
|
||||
//获取当前激活页签索引
|
||||
// 获取当前激活页签索引;没有激活页时返回 -1。
|
||||
int getActiveIndex() const;
|
||||
// 设置当前激活页签索引;若已是当前激活页则直接返回
|
||||
void setActiveIndex(int idx);
|
||||
// 设置当前激活页签索引。
|
||||
// 首次绘制前仅记录默认激活项;绘制后索引必须命中已提交页签。重复激活当前页会直接返回 true。
|
||||
bool setActiveIndex(int idx);
|
||||
// 获取页签数量
|
||||
int count() const;
|
||||
// 通过页签文本查找索引
|
||||
// 通过页签文本查找索引;重复标题时返回第一个命中项,未命中返回 -1。
|
||||
int indexOf(const std::string& tabText) const;
|
||||
//设置脏区并请求重绘
|
||||
// 设置 TabControl 及其当前页签/页面的脏状态。
|
||||
void setDirty(bool dirty) override;
|
||||
//请求父控件重绘
|
||||
// 请求父控件重绘;托管模式下登记 TabControl 为 root。
|
||||
void requestRepaint(Control* parent)override; // 托管模式下登记为 root;非托管模式下局部更新脏按钮/脏页面
|
||||
// 判断页签栏或当前页面中是否存在待提交的脏内容。
|
||||
bool hasManagedDirtySubtree() const override;
|
||||
// 获取页签栏和当前页面的托管重绘覆盖范围。
|
||||
RECT getManagedRepaintCoverageRect() const override;
|
||||
// 获取页签栏和当前页面的持久绘制覆盖范围。
|
||||
RECT getManagedRepaintPersistentCoverageRect() const override;
|
||||
bool canCommitManagedPartialRepaint() const override; // 判断当前 TabControl 是否可安全做局部提交
|
||||
void commitManagedRepaint() override; // 托管收口阶段执行 TabControl 的真正重绘
|
||||
protected:
|
||||
// 将回调期间加入的页签和页内控件在外层事务结束时一次性提交。
|
||||
void flushPendingMutations() override;
|
||||
// 外层统一解算后,TabControl 需要同步刷新其内部页签栏和页面区。
|
||||
void applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect) override;
|
||||
};
|
||||
@@ -85,19 +85,19 @@ private:
|
||||
COLORREF tableBorderClor = TABLE_DEFAULT_BORDER_COLOR; // 表格边框颜色
|
||||
COLORREF tableBkClor = TABLE_DEFAULT_BG_COLOR; // 表格背景颜色
|
||||
|
||||
void initTextWaH(); //初始化文本像素宽度和高度
|
||||
void initButton(); //初始化翻页按钮
|
||||
void initPageNum(); //初始化页码标签
|
||||
void initTextWaH(); // 初始化文本像素宽度和高度,并据此更新列宽/行高缓存
|
||||
void initButton(); // 初始化或刷新上一页/下一页按钮
|
||||
void initPageNum(); // 初始化或刷新页码标签
|
||||
|
||||
void drawTable(); //绘制当前页
|
||||
void drawHeader(); //绘制表头
|
||||
void drawPageNum(); //绘制页码信息
|
||||
void drawButton(); //绘制翻页按钮
|
||||
void drawTable(); // 绘制当前页的数据行
|
||||
void drawHeader(); // 绘制表头区域
|
||||
void drawPageNum(); // 绘制页码信息
|
||||
void drawButton(); // 绘制翻页按钮
|
||||
// 统一解算后的内部应用入口:
|
||||
// Table 需要复用自己已有的 setWidth/setX 等副作用逻辑,因此单独接管应用过程。
|
||||
void applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect) override;
|
||||
private:
|
||||
//用来检查对话框是否模态,此控件不做实现
|
||||
// Table 不是模态对话框,固定返回 false。
|
||||
bool model() const override { return false; };
|
||||
public:
|
||||
StellarX::ControlText textStyle; // 文本样式
|
||||
@@ -105,18 +105,21 @@ public:
|
||||
void setX(int x) override;
|
||||
// 修改表格运行态 Y,并同步内部页脚元素位置
|
||||
void setY(int y) override;
|
||||
// 修改表格运行态宽度;当前支持横向拉伸
|
||||
// 修改表格运行态宽度;当前支持横向拉伸。
|
||||
// 会重新计算列宽、页脚控件位置和托管重绘 coverage。
|
||||
void setWidth(int width) override;
|
||||
// 修改表格运行态高度;当前版本作为行为边界明确忽略纵向 Stretch
|
||||
// 修改表格运行态高度;当前版本作为行为边界明确忽略纵向 Stretch。
|
||||
// 表格高度由表头、行高、页脚和按钮布局共同计算,外部写入会被有意丢弃。
|
||||
void setHeight(int height) override;
|
||||
public:
|
||||
// 构造一个基础表格,后续通过表头和数据初始化内容
|
||||
Table(int x, int y);
|
||||
// 析构表格并释放内部按钮、页码标签等资源。
|
||||
~Table();
|
||||
|
||||
// 绘制表格
|
||||
// 绘制表格背景、表头、当前页数据、页码和翻页按钮。
|
||||
void draw() override;
|
||||
//事件处理
|
||||
// 处理翻页按钮事件,并在页码变化时请求重绘。
|
||||
bool handleEvent(const ExMessage& msg) override;
|
||||
// 只清理分页按钮的 hover / tooltip / 临时按下态,不触发表格整体重绘。
|
||||
bool clearTransientMouseState() override;
|
||||
@@ -125,60 +128,62 @@ public:
|
||||
// 持久 coverage 排除分页按钮 Tooltip 等临时浮层。
|
||||
RECT getManagedRepaintPersistentCoverageRect() const override;
|
||||
|
||||
// 设置表头
|
||||
// 设置表头;会按新的表头列数规范化已有数据行,并重新计算分页和列宽。
|
||||
void setHeaders(std::initializer_list<std::string> headers);
|
||||
// 设置表格数据(单行追加)
|
||||
// 设置表格数据(单行追加)。
|
||||
// 该接口是 append 语义,不会替换已有数据;如需覆盖请先 clearData() 或 resetTable()。
|
||||
void setData(std::vector<std::string> data);
|
||||
// 设置表格数据(多行追加);如需覆盖请先 clearData() 或 resetTable()
|
||||
void setData(std::initializer_list<std::vector<std::string>> data);
|
||||
//设置每页显示的行数
|
||||
// 设置每页显示的行数;小于 1 会被夹紧为 1,并同步修正当前页和总页数。
|
||||
void setRowsPerPage(int rows);
|
||||
//设置是否显示翻页按钮
|
||||
// 设置是否显示翻页按钮。
|
||||
void showPageButton(bool isShow);
|
||||
//设置表格边框颜色
|
||||
// 设置表格边框颜色。
|
||||
void setTableBorder(COLORREF color);
|
||||
//设置表格背景颜色
|
||||
// 设置表格背景颜色。
|
||||
void setTableBk(COLORREF color);
|
||||
//设置填充模式
|
||||
// 设置表格填充模式。
|
||||
void setTableFillMode(StellarX::FillMode mode);
|
||||
//设置线型
|
||||
// 设置表格边框线型。
|
||||
void setTableLineStyle(StellarX::LineStyle style);
|
||||
//设置边框宽度
|
||||
// 设置表格边框宽度。
|
||||
void setTableBorderWidth(int width);
|
||||
//清空表头
|
||||
// 清空表头并标记尺寸缓存失效。
|
||||
void clearHeaders();
|
||||
//清空表格数据
|
||||
// 清空表格数据并重置分页状态。
|
||||
void clearData();
|
||||
//清空表头和数据
|
||||
// 清空表头和数据,恢复为空表状态。
|
||||
void resetTable();
|
||||
//窗口变化丢快照+标脏
|
||||
// 窗口变化时丢弃背景快照并标记表格需要重绘。
|
||||
void onWindowResize() override;
|
||||
|
||||
//************************** 获取属性 *****************************/
|
||||
|
||||
//获取当前页码
|
||||
// 获取当前页码。
|
||||
int getCurrentPage() const;
|
||||
//获取总页数
|
||||
// 获取总页数。
|
||||
int getTotalPages() const;
|
||||
//获取每页显示的行数
|
||||
// 获取每页显示的行数。
|
||||
int getRowsPerPage() const;
|
||||
//获取是否显示翻页按钮
|
||||
// 获取是否显示翻页按钮。
|
||||
bool getShowPageButton() const;
|
||||
//获取表格边框颜色
|
||||
// 获取表格边框颜色。
|
||||
COLORREF getTableBorder() const;
|
||||
//获取表格背景颜色
|
||||
// 获取表格背景颜色。
|
||||
COLORREF getTableBk() const;
|
||||
//获取填充模式
|
||||
// 获取表格填充模式。
|
||||
StellarX::FillMode getTableFillMode() const;
|
||||
//获取线型
|
||||
// 获取表格边框线型。
|
||||
StellarX::LineStyle getTableLineStyle() const;
|
||||
//获取表头
|
||||
// 获取表头副本。
|
||||
std::vector<std::string> getHeaders() const;
|
||||
//获取表格数据
|
||||
// 获取表格数据副本。
|
||||
std::vector<std::vector<std::string>> getData() const;
|
||||
//获取表格边框宽度
|
||||
// 获取表格边框宽度。
|
||||
int getTableBorderWidth() const;
|
||||
//获取表格尺寸
|
||||
// 获取表格当前运行态尺寸;高度为内部内容计算结果,不受 setHeight() 外部写入驱动。
|
||||
int getTableWidth() const;
|
||||
// 获取表格当前运行态高度。
|
||||
int getTableHeight() const;
|
||||
};
|
||||
@@ -37,23 +37,26 @@ public:
|
||||
void draw() override;
|
||||
// 处理输入框鼠标交互和文本录入
|
||||
bool handleEvent(const ExMessage& msg) override;
|
||||
// 设置文本框模式
|
||||
// 设置文本框模式。
|
||||
// PASSWORD_MODE 只改变显示形态:内部仍保存真实文本,绘制时临时生成掩码。
|
||||
void setMode(StellarX::TextBoxmode mode);
|
||||
// 设置可输入最大长度;当前按 std::string 字节长度限制,不等同于 Unicode 字符数
|
||||
// 设置可输入最大长度;当前按 std::string 字节长度限制,不等同于 Unicode 字符数。
|
||||
// len 为 0 时保持原值;截断逻辑会尽量落在完整 MBCS/GBK 字符边界上。
|
||||
void setMaxCharLen(size_t len);
|
||||
// 设置文本框形状;仅支持矩形/圆角矩形,圆形和椭圆会回退为 RECTANGLE
|
||||
// 设置文本框形状;仅支持矩形/圆角矩形,圆形和椭圆会回退为 RECTANGLE。
|
||||
void setTextBoxshape(StellarX::ControlShape shape);
|
||||
// 设置边框颜色
|
||||
// 设置文本框边框颜色。
|
||||
void setTextBoxBorder(COLORREF color);
|
||||
// 设置背景颜色
|
||||
// 设置文本框背景颜色。
|
||||
void setTextBoxBk(COLORREF color);
|
||||
// 设置当前文本
|
||||
// 设置当前文本。
|
||||
// 超过 maxCharLen 时按字节长度截断;已绘制且存在父控件时会请求父级托管重绘。
|
||||
void setText(std::string text);
|
||||
|
||||
// 获取当前文本
|
||||
// 获取当前真实文本;PASSWORD_MODE 下也返回未掩码内容。
|
||||
std::string getText() const;
|
||||
|
||||
private:
|
||||
//用来检查对话框是否模态,此控件不做实现
|
||||
// TextBox 不是模态对话框,固定返回 false。
|
||||
bool model() const override { return false; };
|
||||
};
|
||||
@@ -13,9 +13,6 @@
|
||||
* - WM_GETMINMAXINFO:按最小“客户区”换算到“窗口矩形”,提供系统层最小轨迹值。
|
||||
* - runEventLoop:输入事件先分发给对话框/控件,控件只登记重绘请求;真正绘制在分发或 resize 收口时统一处理。
|
||||
*/
|
||||
//fuck windows
|
||||
//fuck win32
|
||||
//fuck xiaomi
|
||||
#pragma once
|
||||
|
||||
#include "Control.h"
|
||||
@@ -55,6 +52,20 @@ class Window
|
||||
// —— 控件/对话框 ——(容器内的普通控件与非模态对话框)
|
||||
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 // 托管重绘项:记录由哪个控件发起、需要重绘的根控件和覆盖范围(用于后续判断哪些对话框需要补画)
|
||||
@@ -89,8 +100,24 @@ public:
|
||||
void setHeadline(std::string headline); // 设置窗口标题
|
||||
|
||||
// —— 控件/对话框管理 ——(添加到容器,或做存在性判断)
|
||||
void addControl(std::unique_ptr<Control> control); // 添加普通顶层控件
|
||||
void addDialog(std::unique_ptr<Control> dialogs); // 添加非模态对话框
|
||||
// 添加普通顶层控件并返回稳定句柄。
|
||||
// 在事件回调、绘制或其它 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 的非模态对话框
|
||||
|
||||
// —— 访问器 ——(只读接口,供外部查询当前窗口/标题/背景等)
|
||||
@@ -103,26 +130,45 @@ public:
|
||||
COLORREF getBkcolor() const; // 获取纯色背景
|
||||
IMAGE* getBkImage() const; // 获取背景图对象
|
||||
std::string getBkImageFile() const; // 获取背景图路径
|
||||
std::vector<std::unique_ptr<Control>>& getControls(); // 获取普通顶层控件列表
|
||||
|
||||
// —— 尺寸调整 / 托管重绘 ——(事件阶段登记,收口阶段提交)
|
||||
|
||||
// 执行一次 resize 收口 + 统一重绘
|
||||
// 执行一次 resize 收口 + 统一重绘;只在 pending 尺寸或全场景脏标记存在时真正绘制。
|
||||
void pumpResizeIfNeeded();
|
||||
// 供模态 Dialog 上报宿主窗口尺寸变化
|
||||
// 供模态 Dialog 上报宿主窗口尺寸变化;只记录待应用尺寸,实际布局/重绘由窗口收口统一完成。
|
||||
void scheduleResizeFromModal(int w, int h);
|
||||
// 当前是否处于“事件只改状态,不立即画”的阶段
|
||||
// 当前是否处于“事件只改状态,不立即画”的阶段。
|
||||
// 控件在该阶段应请求托管重绘,而不是直接回贴背景或立即 draw。
|
||||
bool isManagedDispatchActive() const;
|
||||
// 记录一笔由 source 发起的托管重绘请求
|
||||
// 记录一笔由 source 发起的托管重绘请求。
|
||||
// Window 会提升到对应 root,并在事件收口时合并 coverage 后统一提交。
|
||||
void requestManagedRepaint(Control* source);
|
||||
// 记录一笔托管重绘请求,并显式并入状态清理前的旧覆盖范围
|
||||
// 记录一笔托管重绘请求,并显式并入状态清理前的旧覆盖范围。
|
||||
// 用于 hover/tooltip 等状态切换,保证旧区域和新区域都会被清理。
|
||||
void requestManagedRepaint(Control* source, const RECT& previousCoverage);
|
||||
// 在事件收口阶段提交本轮登记的 root 重绘
|
||||
// 在事件收口阶段提交本轮登记的 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);
|
||||
@@ -133,7 +179,8 @@ private:
|
||||
};
|
||||
// overlay 层级变化后,同步底层鼠标瞬时状态;内部仍复用既有 synthetic WM_MOUSEMOVE 路径
|
||||
void syncMouseStateAfterOverlayChanged(OverlayMouseSyncMode mode);
|
||||
// 在事件安全点清理已关闭的非模态 Dialog,避免 dialogs 容器长期积累无效对象
|
||||
// 在事件安全点清理已关闭的非模态 Dialog,避免 dialogs 容器长期积累无效对象。
|
||||
// 清理可能触发全场景重绘请求,因此必须放在遍历之外。
|
||||
void sweepClosedDialogs();
|
||||
// 事件回调可能改脏其他 root;flush 前补收集这些跨 root 脏子树
|
||||
void collectDirtyRootsForManagedRepaint();
|
||||
@@ -0,0 +1,82 @@
|
||||
param(
|
||||
[int]$Key
|
||||
)
|
||||
|
||||
# Interactive launcher for the manual KEY1-KEY6 UI regression scenarios.
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$project = Join-Path $root 'imGui-easyX.vcxproj'
|
||||
$executable = Join-Path $root 'build\x64\Debug\imGui-easyX.exe'
|
||||
$msbuild = if ($env:STELLARX_MSBUILD) { $env:STELLARX_MSBUILD } else { 'D:\vs2022\MSBuild\Current\Bin\MSBuild.exe' }
|
||||
$hasKeyArgument = $PSBoundParameters.ContainsKey('Key')
|
||||
|
||||
function Show-KeyMenu {
|
||||
Write-Host ''
|
||||
Write-Host 'StellarX manual UI regression scenarios'
|
||||
Write-Host ' 1 - TabControl and Table redraw'
|
||||
Write-Host ' 2 - Layout, resize, and bit operations'
|
||||
Write-Host ' 3 - Legacy application smoke test'
|
||||
Write-Host ' 4 - Dialog and MessageBox behavior'
|
||||
Write-Host ' 5 - Nested Canvas, Tooltip, and overlay redraw'
|
||||
Write-Host ' 6 - Shape hit-test and uncovered branches'
|
||||
Write-Host ' 0 - Exit'
|
||||
Write-Host ''
|
||||
}
|
||||
|
||||
function Invoke-KeyScenario([int]$scenario) {
|
||||
if ($scenario -lt 1 -or $scenario -gt 6) {
|
||||
throw "KEY must be an integer from 1 to 6: $scenario"
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $msbuild)) {
|
||||
throw "MSBuild not found: $msbuild"
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $project)) {
|
||||
throw "Project not found: $project"
|
||||
}
|
||||
|
||||
Write-Host "Building KEY$scenario (Debug|x64)..." -ForegroundColor Cyan
|
||||
& $msbuild $project /m /t:Rebuild /p:Configuration=Debug /p:Platform=x64 /p:KeyScenario=$scenario /p:EnableAsan=false /v:minimal
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "KEY$scenario build failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $executable)) {
|
||||
throw "Built executable not found: $executable"
|
||||
}
|
||||
|
||||
Write-Host "Launching KEY$scenario. Close the UI window to return to the menu." -ForegroundColor Green
|
||||
$process = Start-Process -FilePath $executable -WorkingDirectory $root -PassThru -Wait
|
||||
if ($process.ExitCode -ne 0) {
|
||||
throw "KEY$scenario exited with code $($process.ExitCode)."
|
||||
}
|
||||
}
|
||||
|
||||
if ($hasKeyArgument) {
|
||||
try {
|
||||
Invoke-KeyScenario $Key
|
||||
exit 0
|
||||
}
|
||||
catch {
|
||||
[Console]::Error.WriteLine("ERROR: $($_.Exception.Message)")
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
while ($true) {
|
||||
Show-KeyMenu
|
||||
$rawSelection = Read-Host 'Select a KEY scenario'
|
||||
$selection = -1
|
||||
if (-not [int]::TryParse($rawSelection, [ref]$selection) -or $selection -lt 0 -or $selection -gt 6) {
|
||||
Write-Host 'Invalid selection. Enter a number from 0 to 6.' -ForegroundColor Yellow
|
||||
continue
|
||||
}
|
||||
if ($selection -eq 0) {
|
||||
exit 0
|
||||
}
|
||||
|
||||
try {
|
||||
Invoke-KeyScenario $selection
|
||||
}
|
||||
catch {
|
||||
Write-Host "ERROR: $($_.Exception.Message)" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
# 外部 UI 驱动:启动确定性 fixture,注入真实鼠标输入,并保存像素与状态诊断。
|
||||
param([int]$Repeat = 1)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$fixture = Join-Path $root 'build\x64\Debug\StellarX.UiFixture.exe'
|
||||
$resultRoot = Join-Path $root 'TestResults\ui'
|
||||
New-Item -ItemType Directory -Force -Path $resultRoot | Out-Null
|
||||
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
# EasyX 控件不是原生子窗口,因此使用 HWND、SendInput 和 PrintWindow 驱动与取证。
|
||||
Add-Type @'
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
public static class StellarXUiNative {
|
||||
// 这些结构只覆盖 SendInput、客户区坐标转换和窗口枚举所需的 Win32 ABI。
|
||||
private delegate bool EnumWindowsProc(IntPtr h, IntPtr parameter);
|
||||
[StructLayout(LayoutKind.Sequential)] public struct POINT { public int X; public int Y; }
|
||||
[StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left; public int Top; public int Right; public int Bottom; }
|
||||
[StructLayout(LayoutKind.Sequential)] private struct MOUSEINPUT {
|
||||
public int Dx; public int Dy; public uint MouseData; public uint Flags; public uint Time; public UIntPtr ExtraInfo;
|
||||
}
|
||||
[StructLayout(LayoutKind.Explicit)] private struct INPUTUNION {
|
||||
[FieldOffset(0)] public MOUSEINPUT Mouse;
|
||||
}
|
||||
[StructLayout(LayoutKind.Sequential)] private struct INPUT {
|
||||
public uint Type; public INPUTUNION Data;
|
||||
}
|
||||
[DllImport("user32.dll", SetLastError=true)] private static extern bool GetClientRect(IntPtr h, out RECT rect);
|
||||
[DllImport("user32.dll", SetLastError=true)] private static extern bool ClientToScreen(IntPtr h, ref POINT point);
|
||||
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
|
||||
[DllImport("user32.dll")] public static extern bool SetCursorPos(int x, int y);
|
||||
[DllImport("user32.dll")] public static extern bool PostMessage(IntPtr h, uint msg, IntPtr w, IntPtr l);
|
||||
[DllImport("user32.dll")] public static extern bool GetCursorPos(out POINT point);
|
||||
[DllImport("user32.dll")] public static extern bool PrintWindow(IntPtr h, IntPtr targetDc, uint flags);
|
||||
[DllImport("user32.dll")] private static extern int GetSystemMetrics(int index);
|
||||
[DllImport("user32.dll", SetLastError=true)] private static extern uint SendInput(uint count, INPUT[] inputs, int size);
|
||||
[DllImport("user32.dll")] private static extern bool EnumWindows(EnumWindowsProc callback, IntPtr parameter);
|
||||
[DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr h, out uint processId);
|
||||
[DllImport("user32.dll", CharSet=CharSet.Unicode)] private static extern int GetWindowTextW(IntPtr h, StringBuilder text, int maxCount);
|
||||
public static IntPtr FindProcessWindow(int processId, string title) {
|
||||
// 同时匹配 PID 和完整标题,避免测试机上其他 EasyX 窗口被误绑定。
|
||||
IntPtr result = IntPtr.Zero;
|
||||
EnumWindows(delegate(IntPtr h, IntPtr parameter) {
|
||||
uint ownerProcessId;
|
||||
GetWindowThreadProcessId(h, out ownerProcessId);
|
||||
if (ownerProcessId != (uint)processId) return true;
|
||||
var text = new StringBuilder(512);
|
||||
GetWindowTextW(h, text, text.Capacity);
|
||||
if (!String.Equals(text.ToString(), title, StringComparison.Ordinal)) return true;
|
||||
result = h;
|
||||
return false;
|
||||
}, IntPtr.Zero);
|
||||
return result;
|
||||
}
|
||||
public static POINT GetClientPoint(IntPtr h, int x, int y) {
|
||||
var point = new POINT { X = x, Y = y };
|
||||
if (!ClientToScreen(h, ref point)) throw new Win32Exception();
|
||||
return point;
|
||||
}
|
||||
public static RECT GetClientRectangle(IntPtr h) {
|
||||
RECT rect;
|
||||
if (!GetClientRect(h, out rect)) throw new Win32Exception();
|
||||
return rect;
|
||||
}
|
||||
public static void ClickLeftMouse() {
|
||||
// 用成对的按下/抬起事件模拟用户点击,事件会进入真实 EasyX 消息循环。
|
||||
var inputs = new INPUT[2];
|
||||
inputs[0].Type = 0;
|
||||
inputs[0].Data.Mouse.Flags = 0x0002;
|
||||
inputs[1].Type = 0;
|
||||
inputs[1].Data.Mouse.Flags = 0x0004;
|
||||
if (SendInput(2, inputs, Marshal.SizeOf(typeof(INPUT))) != 2) throw new Win32Exception();
|
||||
}
|
||||
public static void MoveMouseToScreen(int x, int y) {
|
||||
// SendInput 的绝对坐标按虚拟屏幕换算,兼容多显示器和非零屏幕原点。
|
||||
int left = GetSystemMetrics(76);
|
||||
int top = GetSystemMetrics(77);
|
||||
int width = GetSystemMetrics(78);
|
||||
int height = GetSystemMetrics(79);
|
||||
var input = new INPUT[1];
|
||||
input[0].Type = 0;
|
||||
input[0].Data.Mouse.Dx = (int)Math.Round((x - left) * 65535.0 / Math.Max(1, width - 1));
|
||||
input[0].Data.Mouse.Dy = (int)Math.Round((y - top) * 65535.0 / Math.Max(1, height - 1));
|
||||
input[0].Data.Mouse.Flags = 0xC001;
|
||||
if (SendInput(1, input, Marshal.SizeOf(typeof(INPUT))) != 1) throw new Win32Exception();
|
||||
}
|
||||
}
|
||||
'@
|
||||
|
||||
function Wait-Window([Diagnostics.Process]$process, [string]$title, [int]$timeoutMs = 15000) {
|
||||
# fixture 启动后异步创建窗口;超时或提前退出都应报告明确原因。
|
||||
$watch = [Diagnostics.Stopwatch]::StartNew()
|
||||
while ($watch.ElapsedMilliseconds -lt $timeoutMs) {
|
||||
$process.Refresh()
|
||||
if ($process.HasExited) {
|
||||
throw "Fixture exited before creating '$title' (exit code $($process.ExitCode))"
|
||||
}
|
||||
$h = [StellarXUiNative]::FindProcessWindow($process.Id, $title)
|
||||
if ($h -ne [IntPtr]::Zero) { return $h }
|
||||
Start-Sleep -Milliseconds 50
|
||||
}
|
||||
throw "Window not found: $title"
|
||||
}
|
||||
|
||||
function Client-Point([IntPtr]$h, [int]$x, [int]$y) {
|
||||
# 将 fixture 客户区坐标转换为 SendInput 所需的全局屏幕坐标。
|
||||
return [StellarXUiNative]::GetClientPoint($h, $x, $y)
|
||||
}
|
||||
|
||||
function Move-Mouse([IntPtr]$h, [int]$x, [int]$y) {
|
||||
# 输入坐标以客户区为单位,先转换到屏幕坐标再激活目标窗口。
|
||||
$p = Client-Point $h $x $y
|
||||
[void][StellarXUiNative]::SetForegroundWindow($h)
|
||||
[StellarXUiNative]::MoveMouseToScreen($p.X, $p.Y)
|
||||
Start-Sleep -Milliseconds 80
|
||||
}
|
||||
|
||||
function Click-Mouse([IntPtr]$h, [int]$x, [int]$y) {
|
||||
# 点击后等待一小段时间,让 EasyX 批量处理消息并完成统一重绘收口。
|
||||
Move-Mouse $h $x $y
|
||||
[StellarXUiNative]::ClickLeftMouse()
|
||||
Start-Sleep -Milliseconds 250
|
||||
}
|
||||
|
||||
function Capture-Client([IntPtr]$h, [string]$path) {
|
||||
# 只保存客户区,排除窗口边框;返回 Bitmap 供调用方继续读取关键像素。
|
||||
$rect = [StellarXUiNative]::GetClientRectangle($h)
|
||||
$bitmap = New-Object Drawing.Bitmap ($rect.Right - $rect.Left), ($rect.Bottom - $rect.Top)
|
||||
$graphics = [Drawing.Graphics]::FromImage($bitmap)
|
||||
$dc = $graphics.GetHdc()
|
||||
try {
|
||||
if (-not [StellarXUiNative]::PrintWindow($h, $dc, 1)) {
|
||||
throw 'PrintWindow failed'
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$graphics.ReleaseHdc($dc)
|
||||
$graphics.Dispose()
|
||||
}
|
||||
$bitmap.Save($path, [Drawing.Imaging.ImageFormat]::Png)
|
||||
return $bitmap
|
||||
}
|
||||
|
||||
function Wait-Result([string]$path, [int]$timeoutMs = 15000) {
|
||||
# 等待 fixture 写出 JSON 检查点,并把协议内容解析为 PowerShell 对象。
|
||||
$watch = [Diagnostics.Stopwatch]::StartNew()
|
||||
while ($watch.ElapsedMilliseconds -lt $timeoutMs) {
|
||||
if (Test-Path $path) { return Get-Content -Raw $path | ConvertFrom-Json }
|
||||
Start-Sleep -Milliseconds 50
|
||||
}
|
||||
throw "Result timeout: $path"
|
||||
}
|
||||
|
||||
function Wait-TrueResultProperty([string]$path, [string]$property, [int]$timeoutMs = 15000) {
|
||||
# 对话框等异步场景可能多次更新 JSON,直到目标状态真正变为 true 才继续。
|
||||
$watch = [Diagnostics.Stopwatch]::StartNew()
|
||||
while ($watch.ElapsedMilliseconds -lt $timeoutMs) {
|
||||
if (Test-Path $path) {
|
||||
try {
|
||||
$result = Get-Content -Raw $path | ConvertFrom-Json
|
||||
if ($result.$property -eq $true) { return $result }
|
||||
}
|
||||
catch {
|
||||
# fixture 可能正处于 truncate 与 flush 之间,解析失败时继续轮询到场景超时。
|
||||
}
|
||||
}
|
||||
Start-Sleep -Milliseconds 50
|
||||
}
|
||||
throw "Result property timeout: $path ($property)"
|
||||
}
|
||||
|
||||
function Assert-Rgb($pixel, [int]$r, [int]$g, [int]$b, [string]$message) {
|
||||
# 关键像素用精确颜色断言,避开文本抗锯齿导致的非确定性。
|
||||
if ($pixel.R -ne $r -or $pixel.G -ne $g -or $pixel.B -ne $b) {
|
||||
throw "$message actual=($($pixel.R),$($pixel.G),$($pixel.B)) expected=($r,$g,$b)"
|
||||
}
|
||||
}
|
||||
|
||||
function Assert-NotRgb($pixel, [int]$r, [int]$g, [int]$b, [string]$message) {
|
||||
# 用于验证关闭或移出后,目标像素不再保留指定的控件颜色。
|
||||
if ($pixel.R -eq $r -and $pixel.G -eq $g -and $pixel.B -eq $b) {
|
||||
throw "$message actual=($($pixel.R),$($pixel.G),$($pixel.B))"
|
||||
}
|
||||
}
|
||||
|
||||
function Wait-ClientRgb([IntPtr]$h, [int]$x, [int]$y, [int]$r, [int]$g, [int]$b,
|
||||
[string]$capturePath, [int]$timeoutMs = 15000) {
|
||||
# 某些绘制在消息批次尾才完成,因此对目标像素进行有界轮询而不是固定猜测延时。
|
||||
$watch = [Diagnostics.Stopwatch]::StartNew()
|
||||
$actual = $null
|
||||
while ($watch.ElapsedMilliseconds -lt $timeoutMs) {
|
||||
$probe = Capture-Client $h $capturePath
|
||||
$actual = $probe.GetPixel($x, $y)
|
||||
$matched = $actual.R -eq $r -and $actual.G -eq $g -and $actual.B -eq $b
|
||||
$probe.Dispose()
|
||||
if ($matched) { return }
|
||||
Start-Sleep -Milliseconds 50
|
||||
}
|
||||
throw "Pixel timeout at ($x,$y) actual=($($actual.R),$($actual.G),$($actual.B)) expected=($r,$g,$b)"
|
||||
}
|
||||
|
||||
$savedCursor = New-Object StellarXUiNative+POINT
|
||||
# UI 测试会抢占前台并移动鼠标,外层 finally 必须恢复用户原来的光标位置。
|
||||
[void][StellarXUiNative]::GetCursorPos([ref]$savedCursor)
|
||||
$scenarios = @('ownership', 'geometry', 'tooltip', 'tab', 'table', 'dialog')
|
||||
$sessionRoot = Join-Path $resultRoot ((Get-Date -Format 'yyyyMMdd-HHmmss-fff') + "-$PID")
|
||||
|
||||
try {
|
||||
# 场景按固定顺序串行执行,避免多个 EasyX 图形上下文同时争用桌面。
|
||||
for ($iteration = 1; $iteration -le $Repeat; $iteration++) {
|
||||
foreach ($scenario in $scenarios) {
|
||||
$runDir = Join-Path $sessionRoot "run-$iteration-$scenario"
|
||||
New-Item -ItemType Directory -Force -Path $runDir | Out-Null
|
||||
$resultFile = Join-Path $runDir "$scenario.json"
|
||||
$stdoutFile = Join-Path $runDir 'fixture.stdout.log'
|
||||
$stderrFile = Join-Path $runDir 'fixture.stderr.log'
|
||||
$process = Start-Process -FilePath $fixture -ArgumentList @($scenario, $runDir) -PassThru `
|
||||
-RedirectStandardOutput $stdoutFile -RedirectStandardError $stderrFile
|
||||
$h = [IntPtr]::Zero
|
||||
try {
|
||||
# 先绑定唯一 HWND,再根据场景注入动作并等待 fixture 的 JSON 检查点。
|
||||
$h = Wait-Window $process "StellarX.UI.$scenario"
|
||||
[void][StellarXUiNative]::SetForegroundWindow($h)
|
||||
switch ($scenario) {
|
||||
'ownership' { Click-Mouse $h 80 40; $result = Wait-Result $resultFile; if (-not $result.removed) { throw 'ownership result failed' } }
|
||||
'geometry' { Click-Mouse $h 80 40; $result = Wait-Result $resultFile; if ($result.x -ne 220 -or $result.textWidth -ne 80 -or $result.canvasWidth -ne 80 -or $result.labelVisible) { throw 'geometry result failed' }; Move-Mouse $h 460 300 }
|
||||
'tooltip' { Move-Mouse $h 80 40; Wait-ClientRgb $h 200 61 255 255 255 (Join-Path $runDir 'tooltip-wait.png') }
|
||||
'tab' { Click-Mouse $h 230 35; Click-Mouse $h 395 238; $result = Wait-Result $resultFile; if ($result.active -ne 1 -or $result.firstSelected -or -not $result.secondSelected -or $result.firstPageVisible -or -not $result.secondPageVisible) { throw 'tab uniqueness result failed' } }
|
||||
'table' { Click-Mouse $h 395 268; $result = Wait-Result $resultFile; if ($result.width -le 40 -or $result.coverageRight -le $result.coverageLeft -or $result.coverageBottom -le $result.coverageTop) { throw 'table minimum width or coverage failed' } }
|
||||
'dialog' {
|
||||
Click-Mouse $h 90 40
|
||||
Start-Sleep -Milliseconds 350
|
||||
Click-Mouse $h 320 120
|
||||
$result = Wait-TrueResultProperty $resultFile 'upperClosed'
|
||||
if ($result.lowerClosed) { throw 'lower dialog closed with upper dialog' }
|
||||
$lowerBitmap = Capture-Client $h (Join-Path $runDir 'lower-visible.png')
|
||||
Assert-NotRgb ($lowerBitmap.GetPixel(150, 120)) 30 50 70 'lower dialog was not visible after upper close'
|
||||
$lowerBitmap.Dispose()
|
||||
Click-Mouse $h 240 185
|
||||
$result = Wait-TrueResultProperty $resultFile 'lowerClosed'
|
||||
}
|
||||
}
|
||||
$bitmap = Capture-Client $h (Join-Path $runDir 'screen.png')
|
||||
if ($scenario -eq 'ownership' -or $scenario -eq 'geometry') {
|
||||
Assert-Rgb ($bitmap.GetPixel(80, 40)) 30 50 70 "$scenario old region was not restored"
|
||||
}
|
||||
if ($scenario -eq 'geometry') {
|
||||
Assert-Rgb ($bitmap.GetPixel(260, 40)) 80 160 220 'moved button was not rendered'
|
||||
Assert-Rgb ($bitmap.GetPixel(25, 90)) 30 50 70 'hidden label left stale pixels'
|
||||
Assert-Rgb ($bitmap.GetPixel(30, 130)) 30 50 70 'moved TextBox left stale pixels'
|
||||
Assert-Rgb ($bitmap.GetPixel(230, 130)) 230 230 210 'moved TextBox was not rendered'
|
||||
Assert-Rgb ($bitmap.GetPixel(30, 190)) 30 50 70 'moved Canvas left stale pixels'
|
||||
Assert-Rgb ($bitmap.GetPixel(230, 190)) 40 120 90 'moved Canvas was not rendered'
|
||||
}
|
||||
if ($scenario -eq 'tooltip') {
|
||||
Assert-Rgb ($bitmap.GetPixel(200, 61)) 255 255 255 'stationary tooltip did not appear'
|
||||
Move-Mouse $h 460 300
|
||||
$clearedBitmap = Capture-Client $h (Join-Path $runDir 'tooltip-cleared.png')
|
||||
Assert-Rgb ($clearedBitmap.GetPixel(200, 61)) 30 50 70 'tooltip was not cleared after mouse exit'
|
||||
$clearedBitmap.Dispose()
|
||||
}
|
||||
if ($scenario -eq 'tab') {
|
||||
Assert-Rgb ($bitmap.GetPixel(50, 80)) 110 70 120 'active tab page pixels are incorrect'
|
||||
}
|
||||
if ($scenario -eq 'table') {
|
||||
$outsideX = [Math]::Min(479, [int]$result.coverageRight + 5)
|
||||
Assert-Rgb ($bitmap.GetPixel($outsideX, 30)) 30 50 70 'table drew outside reported horizontal coverage'
|
||||
}
|
||||
if ($scenario -eq 'dialog') {
|
||||
Assert-Rgb ($bitmap.GetPixel(240, 150)) 30 50 70 'dialog pixels remained after both dialogs closed'
|
||||
}
|
||||
$bitmap.Dispose()
|
||||
}
|
||||
catch {
|
||||
# 失败时保留驱动错误、fixture 输出和客户区截图,便于定位像素或状态问题。
|
||||
@{ scenario = $scenario; message = $_.Exception.Message; processId = $process.Id; hwnd = $h.ToInt64() } |
|
||||
ConvertTo-Json | Set-Content -Encoding UTF8 (Join-Path $runDir 'driver-error.json')
|
||||
if ($h -ne [IntPtr]::Zero) {
|
||||
try {
|
||||
$failureBitmap = Capture-Client $h (Join-Path $runDir 'failure.png')
|
||||
$failureBitmap.Dispose()
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
throw
|
||||
}
|
||||
finally {
|
||||
# 正常和异常路径都发送 WM_CLOSE;若 fixture 未退出,再按超时兜底终止其进程。
|
||||
if (-not $process.HasExited) {
|
||||
if ($h -ne [IntPtr]::Zero) {
|
||||
[void][StellarXUiNative]::PostMessage($h, 0x0010, [IntPtr]::Zero, [IntPtr]::Zero)
|
||||
}
|
||||
if (-not $process.WaitForExit(2000)) { $process.Kill() }
|
||||
}
|
||||
$process.Dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
[void][StellarXUiNative]::SetCursorPos($savedCursor.X, $savedCursor.Y)
|
||||
}
|
||||
|
||||
Write-Host "UI tests passed: $Repeat iteration(s)"
|
||||
@@ -0,0 +1,91 @@
|
||||
param(
|
||||
# Unit/Render/UI 可单独执行;NonUI 跳过界面操作;All 是完整回归。
|
||||
[ValidateSet('Unit', 'Render', 'UI', 'NonUI', 'All', 'CI')]
|
||||
[string]$Suite = 'Unit',
|
||||
[ValidateSet('Debug', 'Release')]
|
||||
[string]$Configuration = 'Debug',
|
||||
[ValidateSet('x64', 'Win32')]
|
||||
[string]$Platform = 'x64'
|
||||
)
|
||||
|
||||
# 统一测试入口:负责构建指定矩阵,并把所有诊断结果集中到 TestResults。
|
||||
$ErrorActionPreference = 'Stop'
|
||||
# 暂时清空再恢复 PATH,避免 MSBuild/VSTest 被其他工具链中的同名程序劫持。
|
||||
$savedPath = $env:Path
|
||||
[Environment]::SetEnvironmentVariable('PATH', $null, [EnvironmentVariableTarget]::Process)
|
||||
[Environment]::SetEnvironmentVariable('Path', $savedPath, [EnvironmentVariableTarget]::Process)
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$msbuild = if ($env:STELLARX_MSBUILD) { $env:STELLARX_MSBUILD } else { 'D:\vs2022\MSBuild\Current\Bin\MSBuild.exe' }
|
||||
$vstest = if ($env:STELLARX_VSTEST) { $env:STELLARX_VSTEST } else { 'D:\vs2022\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe' }
|
||||
$results = Join-Path $root 'TestResults'
|
||||
# 测试输出目录允许未跟踪生成 TRX、JSON、日志和截图,不写入源码目录。
|
||||
New-Item -ItemType Directory -Force -Path $results | Out-Null
|
||||
if (-not (Test-Path -LiteralPath $msbuild)) { throw "MSBuild not found: $msbuild" }
|
||||
if (-not (Test-Path -LiteralPath $vstest)) { throw "VSTest not found: $vstest" }
|
||||
|
||||
function Build-Project([string]$project, [string]$config, [string]$arch, [bool]$asan = $false) {
|
||||
# ASan 使用 Rebuild,确保运行库和插桩设置不会沿用上一种构建配置。
|
||||
$asanValue = if ($asan) { 'true' } else { 'false' }
|
||||
$target = if ($asan) { 'Rebuild' } else { 'Build' }
|
||||
& $msbuild $project /m /t:$target /p:Configuration=$config /p:Platform=$arch /p:EnableAsan=$asanValue /v:minimal
|
||||
if ($LASTEXITCODE -ne 0) { throw "Build failed: $project ($config|$arch)" }
|
||||
}
|
||||
|
||||
function Run-Unit([string]$config, [string]$arch, [bool]$asan = $false) {
|
||||
# Native Unit Test 在隔离进程中运行,单个 UAF 回归不会直接拖垮 VSTest 宿主。
|
||||
$project = Join-Path $root 'tests\StellarX.Tests.vcxproj'
|
||||
Build-Project $project $config $arch $asan
|
||||
$outputConfig = if ($asan) { "$config-asan" } else { $config }
|
||||
$dll = Join-Path $root "build\$arch\$outputConfig\StellarX.Tests.dll"
|
||||
$trxName = "unit-$arch-$config" + $(if ($asan) { '-asan' } else { '' }) + '.trx'
|
||||
$savedAsanOptions = $env:ASAN_OPTIONS
|
||||
try {
|
||||
# 只对 ASan 用例启用遇错即停;退出后恢复调用者原有环境变量。
|
||||
if ($asan) { $env:ASAN_OPTIONS = 'halt_on_error=1:detect_leaks=0' }
|
||||
& $vstest $dll /InIsolation /Logger:"trx;LogFileName=$trxName" /ResultsDirectory:$results
|
||||
if ($LASTEXITCODE -ne 0) { throw "Unit tests failed: $dll" }
|
||||
}
|
||||
finally {
|
||||
$env:ASAN_OPTIONS = $savedAsanOptions
|
||||
}
|
||||
}
|
||||
|
||||
function Run-UI([int]$repeat = 1) {
|
||||
# EasyX 依赖交互式桌面,UI fixture 始终固定使用 Debug x64 并串行运行。
|
||||
Build-Project (Join-Path $root 'tests\StellarX.UiFixture.vcxproj') 'Debug' 'x64'
|
||||
& (Join-Path $PSScriptRoot 'run-ui-tests.ps1') -Repeat $repeat
|
||||
if ($LASTEXITCODE -ne 0) { throw 'UI tests failed' }
|
||||
}
|
||||
|
||||
function Build-KeyScenarios {
|
||||
# KEY1-KEY6 是应用现有主路径的编译/链接冒烟,不在这里模拟鼠标交互。
|
||||
$project = Join-Path $root 'imGui-easyX.vcxproj'
|
||||
foreach ($key in 1..6) {
|
||||
& $msbuild $project /m /t:Build /p:Configuration=Debug /p:Platform=x64 /p:KeyScenario=$key /p:EnableAsan=false /v:minimal
|
||||
if ($LASTEXITCODE -ne 0) { throw "KEY$key build failed" }
|
||||
}
|
||||
}
|
||||
|
||||
function Run-NonUI {
|
||||
# 提交前非交互矩阵:覆盖内存安全、多架构、Release 和现有 KEY 编译场景。
|
||||
Run-Unit 'Debug' 'x64'
|
||||
Run-Unit 'Debug' 'x64' $true
|
||||
Run-Unit 'Release' 'x64'
|
||||
Run-Unit 'Debug' 'Win32'
|
||||
Build-KeyScenarios
|
||||
}
|
||||
|
||||
switch ($Suite) {
|
||||
# Render 当前复用真实 UI fixture 的像素检查;All 在 NonUI 基础上追加三轮 UI。
|
||||
'Unit' { Run-Unit $Configuration $Platform }
|
||||
'Render' { Run-UI 1 }
|
||||
'UI' { Run-UI 1 }
|
||||
'NonUI' { Run-NonUI }
|
||||
'All' { Run-NonUI; Run-UI 3 }
|
||||
'CI' {
|
||||
Run-Unit 'Debug' 'x64'
|
||||
Run-Unit 'Release' 'x64'
|
||||
Run-Unit 'Debug' 'Win32'
|
||||
if ($env:STELLARX_UI_TESTS -eq '1') { Run-UI 1 }
|
||||
}
|
||||
}
|
||||
@@ -144,6 +144,17 @@ void Button::setTooltipTextsForToggle(const std::string& onText, const std::stri
|
||||
tipUserOverride = true;
|
||||
}
|
||||
|
||||
void Button::enableTooltip(bool on)
|
||||
{
|
||||
if (tipEnabled == on)
|
||||
return;
|
||||
if (!on)
|
||||
hideTooltip();
|
||||
tipEnabled = on;
|
||||
if (on && hover)
|
||||
tipHoverTick = GetTickCount64();
|
||||
}
|
||||
|
||||
void Button::initButton(const std::string text, StellarX::ButtonMode mode, StellarX::ControlShape shape, COLORREF ct, COLORREF cf, COLORREF ch)
|
||||
{
|
||||
this->id = "Button";
|
||||
@@ -196,7 +207,7 @@ void Button::draw()
|
||||
//设置字体颜色
|
||||
settextcolor(drawTextStyle.color);
|
||||
//设置字体样式
|
||||
settextstyle(drawTextStyle.nHeight, drawTextStyle.nWidth, drawTextStyle.lpszFace,
|
||||
settextstyle(drawTextStyle.nHeight, drawTextStyle.nWidth, drawTextStyle.faceName.c_str(),
|
||||
drawTextStyle.nEscapement, drawTextStyle.nOrientation, drawTextStyle.nWeight,
|
||||
drawTextStyle.bItalic, drawTextStyle.bUnderline, drawTextStyle.bStrikeOut);
|
||||
|
||||
@@ -221,8 +232,12 @@ void Button::draw()
|
||||
|
||||
//设置按钮填充模式
|
||||
setfillstyle((int)buttonFillMode, (int)buttonFillIma, buttonFileIMAGE.get());
|
||||
if ((saveBkX != this->x) || (saveBkY != this->y) || (!hasSnap) || (saveWidth != this->width) || (saveHeight != this->height) || !saveBkImage)
|
||||
saveBackground(this->x, this->y, (this->width + bordWith), (this->height + bordHeight));
|
||||
const int snapshotWidth = this->width + bordWith;
|
||||
const int snapshotHeight = this->height + bordHeight;
|
||||
// 快照尺寸与按钮实际 coverage 使用同一组值,避免边框或右下边界没有被恢复。
|
||||
if ((saveBkX != this->x) || (saveBkY != this->y) || (!hasSnap) ||
|
||||
(saveWidth != snapshotWidth) || (saveHeight != snapshotHeight) || !saveBkImage)
|
||||
saveBackground(this->x, this->y, snapshotWidth, snapshotHeight);
|
||||
// 恢复背景(清除旧内容)
|
||||
restBackground();
|
||||
//根据按钮形状绘制
|
||||
@@ -282,6 +297,7 @@ bool Button::handleEvent(const ExMessage& msg)
|
||||
{
|
||||
if (!show)
|
||||
return false;
|
||||
auto mutationScope = enterMutationScope();
|
||||
resetEventVisualChanged();
|
||||
|
||||
bool oldHover = hover;// 注意:只在状态变化时记录,避免 WM_MOUSEMOVE 刷屏
|
||||
@@ -397,29 +413,7 @@ bool Button::handleEvent(const ExMessage& msg)
|
||||
{
|
||||
// 到点就显示
|
||||
if (GetTickCount64() - tipHoverTick >= (ULONGLONG)tipDelayMs)
|
||||
{
|
||||
tipVisible = true;
|
||||
|
||||
// 定位(跟随鼠标 or 相对按钮)
|
||||
int tipX = tipFollowCursor ? (lastMouseX + tipOffsetX) : lastMouseX;
|
||||
int tipY = tipFollowCursor ? (lastMouseY + tipOffsetY) : y + height;
|
||||
// 设置文本(用户可能动态改了提示文本
|
||||
if (tipUserOverride)
|
||||
{
|
||||
if (mode == StellarX::ButtonMode::NORMAL)
|
||||
tipLabel.setText(tipTextClick);
|
||||
else if (mode == StellarX::ButtonMode::TOGGLE)
|
||||
tipLabel.setText(click ? tipTextOn : tipTextOff);
|
||||
}
|
||||
else
|
||||
if (mode == StellarX::ButtonMode::TOGGLE)
|
||||
tipLabel.setText(click ? tipTextOn : tipTextOff);
|
||||
// 设置位置
|
||||
tipLabel.setX(tipX);
|
||||
tipLabel.setY(tipY);
|
||||
// 标记需要绘制
|
||||
tipLabel.setDirty(true);
|
||||
}
|
||||
showTooltipAtLastMouse();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,6 +449,19 @@ bool Button::handleEvent(const ExMessage& msg)
|
||||
return consume;
|
||||
}
|
||||
|
||||
void Button::tick(ULONGLONG nowMs)
|
||||
{
|
||||
// tick 只处理已经命中的按钮;到期后显示 Tooltip,并把重绘交给父容器统一提交。
|
||||
if (!show || !tipEnabled || !hover || tipVisible)
|
||||
return;
|
||||
if (nowMs - tipHoverTick < static_cast<ULONGLONG>(tipDelayMs))
|
||||
return;
|
||||
showTooltipAtLastMouse();
|
||||
dirty = true;
|
||||
markEventVisualChanged(true);
|
||||
requestRepaint(parent);
|
||||
}
|
||||
|
||||
bool Button::clearTransientMouseState()
|
||||
{
|
||||
if (!show)
|
||||
@@ -516,6 +523,8 @@ bool Button::clearTransientMouseState()
|
||||
RECT Button::getManagedRepaintCoverageRect() const
|
||||
{
|
||||
RECT coverage = getBoundsRect();
|
||||
coverage.right += bordWith;
|
||||
coverage.bottom += bordHeight;
|
||||
|
||||
if (!tipEnabled || !tipVisible)
|
||||
return coverage;
|
||||
@@ -532,7 +541,10 @@ RECT Button::getManagedRepaintPersistentCoverageRect() const
|
||||
{
|
||||
// Tooltip 属于临时浮层,只参与完整 coverage 和上层 overlay 判断;
|
||||
// 兄弟控件是否需要作废背景快照,只应由按钮本体这类持久绘制范围决定。
|
||||
return getBoundsRect();
|
||||
RECT coverage = getBoundsRect();
|
||||
coverage.right += bordWith;
|
||||
coverage.bottom += bordHeight;
|
||||
return coverage;
|
||||
}
|
||||
|
||||
void Button::setOnClickListener(std::function<void()> callback)
|
||||
@@ -640,6 +652,7 @@ void Button::setButtonShape(StellarX::ControlShape shape)
|
||||
//允许通过外部函数修改按钮的点击状态,并执行相应的回调函数
|
||||
void Button::setButtonClick(BOOL click)
|
||||
{
|
||||
auto mutationScope = enterMutationScope();
|
||||
const bool targetClick = (click != FALSE);
|
||||
// TOGGLE 状态若没有发生变化,就不应重复触发 onToggleOn/OffCallback。
|
||||
// 否则像 TabControl 这种“外部再次激活已激活页签”的场景,
|
||||
@@ -769,6 +782,7 @@ void Button::cutButtonText()
|
||||
|
||||
void Button::hideTooltip()
|
||||
{
|
||||
// 隐藏提示时先回贴其独立快照,再作废状态,避免 Tooltip 像素留在按钮之外。
|
||||
if (tipVisible)
|
||||
{
|
||||
tipVisible = false;
|
||||
@@ -781,6 +795,30 @@ void Button::hideTooltip()
|
||||
}
|
||||
}
|
||||
|
||||
void Button::showTooltipAtLastMouse()
|
||||
{
|
||||
// Tooltip 的位置和文案在真正显示时计算,保证 Toggle 状态与最后一次鼠标位置一致。
|
||||
if (!tipEnabled || tipVisible)
|
||||
return;
|
||||
tipVisible = true;
|
||||
const int tipX = tipFollowCursor ? lastMouseX + tipOffsetX : lastMouseX;
|
||||
const int tipY = tipFollowCursor ? lastMouseY + tipOffsetY : y + height;
|
||||
if (tipUserOverride)
|
||||
{
|
||||
if (mode == StellarX::ButtonMode::NORMAL)
|
||||
tipLabel.setText(tipTextClick);
|
||||
else if (mode == StellarX::ButtonMode::TOGGLE)
|
||||
tipLabel.setText(click ? tipTextOn : tipTextOff);
|
||||
}
|
||||
else if (mode == StellarX::ButtonMode::TOGGLE)
|
||||
{
|
||||
tipLabel.setText(click ? tipTextOn : tipTextOff);
|
||||
}
|
||||
tipLabel.setX(tipX);
|
||||
tipLabel.setY(tipY);
|
||||
tipLabel.setDirty(true);
|
||||
}
|
||||
|
||||
void Button::refreshTooltipTextForState()
|
||||
{
|
||||
if (tipUserOverride) return; // 用户显式设置过 tipText,保持不变
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "Canvas.h"
|
||||
#include "SxLog.h"
|
||||
#include "Window.h"
|
||||
#include <algorithm>
|
||||
|
||||
static bool SxIsNoisyMsg(UINT m)
|
||||
{
|
||||
@@ -81,32 +82,64 @@ void Canvas::relayoutManagedChildren()
|
||||
|
||||
void Canvas::setX(int x)
|
||||
{
|
||||
const RECT previousCoverage = getManagedRepaintCoverageRect();
|
||||
const bool wasDrawn = hasValidBackgroundSnapshot();
|
||||
if (wasDrawn) discardBackground();
|
||||
// 公开 setter 在 Canvas 上不能再视为“单纯改自己的 x”:
|
||||
// 一旦容器移动,子控件的世界坐标也必须整体重算。
|
||||
applyRuntimeRectDirect(x, y, width, height);
|
||||
relayoutManagedChildren();
|
||||
onWindowResize();
|
||||
if (wasDrawn)
|
||||
{
|
||||
if (auto* host = getHostWindow()) host->requestManagedRepaint(this, previousCoverage);
|
||||
else requestRepaint(parent);
|
||||
}
|
||||
}
|
||||
|
||||
void Canvas::setY(int y)
|
||||
{
|
||||
const RECT previousCoverage = getManagedRepaintCoverageRect();
|
||||
const bool wasDrawn = hasValidBackgroundSnapshot();
|
||||
if (wasDrawn) discardBackground();
|
||||
applyRuntimeRectDirect(this->x, y, width, height);
|
||||
relayoutManagedChildren();
|
||||
onWindowResize();
|
||||
if (wasDrawn)
|
||||
{
|
||||
if (auto* host = getHostWindow()) host->requestManagedRepaint(this, previousCoverage);
|
||||
else requestRepaint(parent);
|
||||
}
|
||||
}
|
||||
|
||||
void Canvas::setWidth(int width)
|
||||
{
|
||||
applyRuntimeRectDirect(x, y, width, height);
|
||||
const RECT previousCoverage = getManagedRepaintCoverageRect();
|
||||
const bool wasDrawn = hasValidBackgroundSnapshot();
|
||||
if (wasDrawn) discardBackground();
|
||||
applyRuntimeRectDirect(x, y, (std::max)(0, width), height);
|
||||
relayoutManagedChildren();
|
||||
onWindowResize();
|
||||
if (wasDrawn)
|
||||
{
|
||||
if (auto* host = getHostWindow()) host->requestManagedRepaint(this, previousCoverage);
|
||||
else requestRepaint(parent);
|
||||
}
|
||||
}
|
||||
|
||||
void Canvas::setHeight(int height)
|
||||
{
|
||||
applyRuntimeRectDirect(x, y, width, height);
|
||||
const RECT previousCoverage = getManagedRepaintCoverageRect();
|
||||
const bool wasDrawn = hasValidBackgroundSnapshot();
|
||||
if (wasDrawn) discardBackground();
|
||||
applyRuntimeRectDirect(x, y, width, (std::max)(0, height));
|
||||
relayoutManagedChildren();
|
||||
onWindowResize();
|
||||
if (wasDrawn)
|
||||
{
|
||||
if (auto* host = getHostWindow()) host->requestManagedRepaint(this, previousCoverage);
|
||||
else requestRepaint(parent);
|
||||
}
|
||||
}
|
||||
|
||||
void Canvas::applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect)
|
||||
@@ -117,13 +150,81 @@ void Canvas::applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect)
|
||||
relayoutManagedChildren();
|
||||
}
|
||||
|
||||
void Canvas::clearAllControls()
|
||||
void Canvas::beginMutationScope()
|
||||
{
|
||||
controls.clear();
|
||||
++mutationScopeDepth;
|
||||
Control::beginMutationScope();
|
||||
}
|
||||
|
||||
void Canvas::endMutationScope()
|
||||
{
|
||||
if (mutationScopeDepth > 0)
|
||||
--mutationScopeDepth;
|
||||
Window* host = getHostWindow();
|
||||
if (mutationScopeDepth == 0 && !host)
|
||||
flushPendingMutations();
|
||||
// 向外层传播结束事务可能触发当前 Canvas 被删除,因此这里必须是本函数的最后一步。
|
||||
Control::endMutationScope();
|
||||
}
|
||||
|
||||
void Canvas::flushPendingMutations()
|
||||
{
|
||||
// 先按入队顺序应用 Add/Remove/Clear,再递归提交子容器,保证一次回调的结果可预测。
|
||||
if (applyingControlMutations)
|
||||
return;
|
||||
applyingControlMutations = true;
|
||||
bool changed = false;
|
||||
for (auto& mutation : pendingControlMutations)
|
||||
{
|
||||
switch (mutation.kind)
|
||||
{
|
||||
case ControlMutationKind::Add:
|
||||
if (mutation.control)
|
||||
{
|
||||
mutation.control->setParent(this);
|
||||
const StellarX::ResolvedLayoutRect rect = mutation.control->resolveLayoutRect(
|
||||
localWidth, localHeight, x, y, width, height);
|
||||
mutation.control->applyResolvedLayoutRect(rect);
|
||||
controls.push_back(std::move(mutation.control));
|
||||
changed = true;
|
||||
}
|
||||
break;
|
||||
case ControlMutationKind::Remove:
|
||||
{
|
||||
auto it = std::find_if(controls.begin(), controls.end(), [&](const auto& item)
|
||||
{ return item && item->getHandle() == mutation.handle; });
|
||||
if (it != controls.end())
|
||||
{
|
||||
controls.erase(it);
|
||||
changed = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ControlMutationKind::Clear:
|
||||
if (!controls.empty())
|
||||
{
|
||||
controls.clear();
|
||||
changed = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
pendingControlMutations.clear();
|
||||
for (auto& child : controls)
|
||||
child->flushPendingMutations();
|
||||
applyingControlMutations = false;
|
||||
if (changed)
|
||||
{
|
||||
// 子树结构变化会使既有快照失去可信的覆盖关系,升级为宿主窗口的一次整场景重绘。
|
||||
dirty = true;
|
||||
if (auto* host = getHostWindow()) host->requestFullSceneRedraw();
|
||||
else if (parent) parent->setDirty(true);
|
||||
}
|
||||
}
|
||||
|
||||
void Canvas::draw()
|
||||
{
|
||||
auto mutationScope = enterMutationScope();
|
||||
if (!dirty || !show)
|
||||
{
|
||||
for (auto& control : controls)
|
||||
@@ -190,6 +291,7 @@ void Canvas::draw()
|
||||
bool Canvas::handleEvent(const ExMessage& msg)
|
||||
{
|
||||
if (!show) return false;
|
||||
auto mutationScope = enterMutationScope();
|
||||
resetEventVisualChanged();
|
||||
|
||||
bool consumed = false;
|
||||
@@ -284,8 +386,22 @@ bool Canvas::clearTransientMouseState()
|
||||
}
|
||||
|
||||
|
||||
void Canvas::addControl(std::unique_ptr<Control> control)
|
||||
StellarX::ControlHandle Canvas::addControl(std::unique_ptr<Control> control)
|
||||
{
|
||||
if (!control)
|
||||
return {};
|
||||
const StellarX::ControlHandle handle = control->getHandle();
|
||||
Window* host = getHostWindow();
|
||||
if (!applyingControlMutations && (mutationScopeDepth > 0 || (host && host->isMutationTransactionActive())))
|
||||
{
|
||||
// 事务内只转移所有权并返回句柄,实际挂接父指针和布局在提交阶段完成。
|
||||
PendingControlMutation mutation;
|
||||
mutation.kind = ControlMutationKind::Add;
|
||||
mutation.handle = handle;
|
||||
mutation.control = std::move(control);
|
||||
pendingControlMutations.push_back(std::move(mutation));
|
||||
return handle;
|
||||
}
|
||||
control->setParent(this);
|
||||
// 新子控件加入容器时,立刻按“当前容器运行态矩形”解算一次,
|
||||
// 避免后续第一次 draw / resize 前 world 坐标仍停留在设计态。
|
||||
@@ -304,6 +420,73 @@ void Canvas::addControl(std::unique_ptr<Control> control)
|
||||
|
||||
controls.push_back(std::move(control));
|
||||
dirty = true;
|
||||
return handle;
|
||||
}
|
||||
|
||||
void Canvas::tick(ULONGLONG nowMs)
|
||||
{
|
||||
if (!show)
|
||||
return;
|
||||
auto mutationScope = enterMutationScope();
|
||||
for (auto& control : controls)
|
||||
if (control && control->IsVisible()) control->tick(nowMs);
|
||||
}
|
||||
|
||||
bool Canvas::removeControl(StellarX::ControlHandle handle)
|
||||
{
|
||||
if (!handle)
|
||||
return false;
|
||||
bool exists = std::find_if(controls.begin(), controls.end(), [&](const auto& item)
|
||||
{ return item && item->getHandle() == handle; }) != controls.end();
|
||||
for (const auto& mutation : pendingControlMutations)
|
||||
{
|
||||
if (mutation.kind == ControlMutationKind::Clear)
|
||||
exists = false;
|
||||
else if (mutation.handle == handle)
|
||||
exists = mutation.kind == ControlMutationKind::Add;
|
||||
}
|
||||
if (!exists)
|
||||
return false;
|
||||
Window* host = getHostWindow();
|
||||
if (!applyingControlMutations && (mutationScopeDepth > 0 || (host && host->isMutationTransactionActive())))
|
||||
{
|
||||
pendingControlMutations.push_back({ ControlMutationKind::Remove, handle, nullptr });
|
||||
return true;
|
||||
}
|
||||
pendingControlMutations.push_back({ ControlMutationKind::Remove, handle, nullptr });
|
||||
flushPendingMutations();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Canvas::clearControls()
|
||||
{
|
||||
Window* host = getHostWindow();
|
||||
pendingControlMutations.push_back({ ControlMutationKind::Clear, {}, nullptr });
|
||||
if (!applyingControlMutations && mutationScopeDepth == 0 && (!host || !host->isMutationTransactionActive()))
|
||||
flushPendingMutations();
|
||||
}
|
||||
|
||||
Control* Canvas::findControl(StellarX::ControlHandle handle)
|
||||
{
|
||||
auto it = std::find_if(controls.begin(), controls.end(), [&](const auto& item)
|
||||
{ return item && item->getHandle() == handle; });
|
||||
return it == controls.end() ? nullptr : it->get();
|
||||
}
|
||||
|
||||
const Control* Canvas::findControl(StellarX::ControlHandle handle) const
|
||||
{
|
||||
auto it = std::find_if(controls.begin(), controls.end(), [&](const auto& item)
|
||||
{ return item && item->getHandle() == handle; });
|
||||
return it == controls.end() ? nullptr : it->get();
|
||||
}
|
||||
|
||||
std::vector<StellarX::ControlHandle> Canvas::getControlHandles() const
|
||||
{
|
||||
std::vector<StellarX::ControlHandle> handles;
|
||||
handles.reserve(controls.size());
|
||||
for (const auto& control : controls)
|
||||
if (control) handles.push_back(control->getHandle());
|
||||
return handles;
|
||||
}
|
||||
|
||||
void Canvas::setShape(StellarX::ControlShape shape)
|
||||
@@ -172,46 +172,18 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
StellarX::ControlText& StellarX::ControlText::operator=(const ControlText& text)
|
||||
bool StellarX::ControlText::operator==(const ControlText& text) const
|
||||
{
|
||||
{
|
||||
nHeight = text.nHeight;
|
||||
nWidth = text.nWidth;
|
||||
lpszFace = text.lpszFace;
|
||||
color = text.color;
|
||||
nEscapement = text.nEscapement;
|
||||
nOrientation = text.nOrientation;
|
||||
nWeight = text.nWeight;
|
||||
bItalic = text.bItalic;
|
||||
bUnderline = text.bUnderline;
|
||||
bStrikeOut = text.bStrikeOut;
|
||||
return *this;
|
||||
}
|
||||
}
|
||||
|
||||
bool StellarX::ControlText::operator!=(const ControlText& text)
|
||||
{
|
||||
if(nHeight != text.nHeight)
|
||||
return true;
|
||||
else if (nWidth != text.nWidth)
|
||||
return true;
|
||||
else if (lpszFace != text.lpszFace)
|
||||
return true;
|
||||
else if (color != text.color)
|
||||
return true;
|
||||
else if (nEscapement != text.nEscapement)
|
||||
return true;
|
||||
else if (nOrientation != text.nOrientation)
|
||||
return true;
|
||||
else if (nWeight != text.nWeight)
|
||||
return true;
|
||||
else if (bItalic != text.bItalic)
|
||||
return true;
|
||||
else if (bUnderline != text.bUnderline)
|
||||
return true;
|
||||
else if (bStrikeOut != text.bStrikeOut)
|
||||
return true;
|
||||
return false;
|
||||
return nHeight == text.nHeight &&
|
||||
nWidth == text.nWidth &&
|
||||
faceName == text.faceName &&
|
||||
color == text.color &&
|
||||
nEscapement == text.nEscapement &&
|
||||
nOrientation == text.nOrientation &&
|
||||
nWeight == text.nWeight &&
|
||||
bItalic == text.bItalic &&
|
||||
bUnderline == text.bUnderline &&
|
||||
bStrikeOut == text.bStrikeOut;
|
||||
}
|
||||
void Control::setIsVisible(bool show)
|
||||
{
|
||||
@@ -345,26 +317,66 @@ const StellarX::LayoutCapability& Control::getLayoutCapability() const
|
||||
|
||||
void Control::setX(int x)
|
||||
{
|
||||
// 先保存旧 coverage 并回贴旧快照,再写入新坐标;这样移动控件不会留下旧像素。
|
||||
const RECT previousCoverage = getManagedRepaintCoverageRect();
|
||||
const bool wasDrawn = hasValidBackgroundSnapshot();
|
||||
if (wasDrawn)
|
||||
discardBackground();
|
||||
this->x = x;
|
||||
dirty = true;
|
||||
if (wasDrawn)
|
||||
{
|
||||
if (auto* host = getHostWindow()) host->requestManagedRepaint(this, previousCoverage);
|
||||
else requestRepaint(parent);
|
||||
}
|
||||
}
|
||||
|
||||
void Control::setY(int y)
|
||||
{
|
||||
// Y 方向与 X 方向使用同一生命周期顺序,旧区域和新区域都由托管重绘统一收口。
|
||||
const RECT previousCoverage = getManagedRepaintCoverageRect();
|
||||
const bool wasDrawn = hasValidBackgroundSnapshot();
|
||||
if (wasDrawn)
|
||||
discardBackground();
|
||||
this->y = y;
|
||||
dirty = true;
|
||||
if (wasDrawn)
|
||||
{
|
||||
if (auto* host = getHostWindow()) host->requestManagedRepaint(this, previousCoverage);
|
||||
else requestRepaint(parent);
|
||||
}
|
||||
}
|
||||
|
||||
void Control::setWidth(int width)
|
||||
{
|
||||
this->width = width;
|
||||
// 尺寸变化会改变快照大小,必须先释放旧快照,避免新绘制复用错误尺寸的背景。
|
||||
const RECT previousCoverage = getManagedRepaintCoverageRect();
|
||||
const bool wasDrawn = hasValidBackgroundSnapshot();
|
||||
if (wasDrawn)
|
||||
discardBackground();
|
||||
this->width = (std::max)(0, width);
|
||||
dirty = true;
|
||||
if (wasDrawn)
|
||||
{
|
||||
if (auto* host = getHostWindow()) host->requestManagedRepaint(this, previousCoverage);
|
||||
else requestRepaint(parent);
|
||||
}
|
||||
}
|
||||
|
||||
void Control::setHeight(int height)
|
||||
{
|
||||
this->height = height;
|
||||
// 高度调整同样登记变化前的 coverage,供 Window 在提交阶段覆盖旧、新两个区域。
|
||||
const RECT previousCoverage = getManagedRepaintCoverageRect();
|
||||
const bool wasDrawn = hasValidBackgroundSnapshot();
|
||||
if (wasDrawn)
|
||||
discardBackground();
|
||||
this->height = (std::max)(0, height);
|
||||
dirty = true;
|
||||
if (wasDrawn)
|
||||
{
|
||||
if (auto* host = getHostWindow()) host->requestManagedRepaint(this, previousCoverage);
|
||||
else requestRepaint(parent);
|
||||
}
|
||||
}
|
||||
|
||||
bool Control::clearTransientMouseState()
|
||||
@@ -526,7 +538,25 @@ void Control::onRequestRepaintAsRoot()
|
||||
bool Control::shouldDeferManagedRepaint() const
|
||||
{
|
||||
Window* host = getHostWindow();
|
||||
return host && host->isManagedDispatchActive();
|
||||
return host && (host->isManagedDispatchActive() || host->isMutationTransactionActive());
|
||||
}
|
||||
|
||||
void Control::beginMutationScope()
|
||||
{
|
||||
// 子控件不单独维护顶层事务,沿 parent 链把计数交给 Canvas 或 Window。
|
||||
if (parent)
|
||||
parent->beginMutationScope();
|
||||
else if (auto* host = getHostWindow())
|
||||
host->beginMutationScope();
|
||||
}
|
||||
|
||||
void Control::endMutationScope()
|
||||
{
|
||||
// 与 beginMutationScope 对称;最外层 root 结束后才有资格提交待处理队列。
|
||||
if (parent)
|
||||
parent->endMutationScope();
|
||||
else if (auto* host = getHostWindow())
|
||||
host->endMutationScope();
|
||||
}
|
||||
|
||||
// 获取宿主 Window:
|
||||
@@ -614,6 +644,7 @@ void Control::saveBackground(int x, int y, int w, int h)
|
||||
|
||||
if (w <= 0 || h <= 0) return;
|
||||
saveBkX = x; saveBkY = y; saveWidth = w; saveHeight = h;
|
||||
// 快照描述的是下一次局部恢复所需的屏幕背景,位置或尺寸变化时不能继续复用旧缓存。
|
||||
if (saveBkImage)
|
||||
{
|
||||
//尺寸变了才重建,避免反复 new/delete
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "Dialog.h"
|
||||
#include "SxLog.h"
|
||||
#include "SxEventLoopInternal.h"
|
||||
|
||||
Dialog::Dialog(Window& h, std::string text, std::string message, StellarX::MessageBoxType type, bool modal)
|
||||
: Canvas(), message(message), type(type), modal(modal), hWnd(h), titleText(text)
|
||||
@@ -52,7 +53,7 @@ void Dialog::draw()
|
||||
setbkmode(TRANSPARENT);
|
||||
|
||||
//设置字体样式
|
||||
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
|
||||
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.faceName.c_str(),
|
||||
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
|
||||
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut);
|
||||
|
||||
@@ -181,6 +182,8 @@ void Dialog::Show()
|
||||
|
||||
while (show)
|
||||
{
|
||||
// 模态对话框保留自己的轮询循环,但消息同样批量处理并在每轮末尾统一绘制。
|
||||
int processedMessages = 0;
|
||||
// ① 轮询窗口尺寸(不依赖 WM_SIZE)
|
||||
RECT rc;
|
||||
GetClientRect(hWnd.getHwnd(), &rc);
|
||||
@@ -205,12 +208,13 @@ void Dialog::Show()
|
||||
|
||||
// ② 处理这只对话框的鼠标/键盘(沿用原来 EX_MOUSE | EX_KEY)
|
||||
ExMessage msg;
|
||||
if (peekmessage(&msg, EX_MOUSE | EX_KEY))
|
||||
{
|
||||
handleEvent(msg);
|
||||
if (!show)
|
||||
break;
|
||||
}
|
||||
const auto batch = StellarX::Internal::drainMessageBatch(msg,
|
||||
[](ExMessage& next) { return peekmessage(&next, EX_MOUSE | EX_KEY); },
|
||||
[&](const ExMessage& next) {
|
||||
handleEvent(next);
|
||||
return show;
|
||||
});
|
||||
processedMessages = static_cast<int>(batch.processed);
|
||||
|
||||
// ③ 最后一笔:只画这只模态,保证永远在最上层
|
||||
if (dirty)
|
||||
@@ -221,7 +225,8 @@ void Dialog::Show()
|
||||
dirty = false;
|
||||
}
|
||||
|
||||
Sleep(10);
|
||||
if (batch.shouldSleep())
|
||||
Sleep(1);
|
||||
}
|
||||
|
||||
if (pendingCleanup && !isCleaning)
|
||||
@@ -236,13 +241,20 @@ void Dialog::Close()
|
||||
{
|
||||
if (!show) return;
|
||||
|
||||
// 关闭只改变可见状态并登记一次全场景收口;对象和快照清理延后到安全阶段执行。
|
||||
show = false;
|
||||
dirty = true;
|
||||
pendingCleanup = true; // 只标记需要清理,不立即执行
|
||||
hWnd.dialogClose = true;
|
||||
hWnd.requestFullSceneRedraw();
|
||||
|
||||
// 工厂模式下非模态触发回调 返回结果
|
||||
if (resultCallback && !modal)
|
||||
{
|
||||
// 回调可能继续修改宿主控件树,因此也纳入 MutationScope,禁止遍历期间立即销毁对象。
|
||||
auto mutationScope = enterMutationScope();
|
||||
resultCallback(this->result);
|
||||
}
|
||||
}
|
||||
|
||||
void Dialog::recenterInHostWindow()
|
||||
@@ -538,7 +550,7 @@ void Dialog::splitMessageLines()
|
||||
void Dialog::getTextSize()
|
||||
{
|
||||
saveStyle();
|
||||
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
|
||||
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.faceName.c_str(),
|
||||
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
|
||||
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut);
|
||||
this->textHeight = 0;
|
||||
@@ -607,7 +619,7 @@ void Dialog::initDialogSize()
|
||||
// 计算文本区域宽度(包括边距)
|
||||
int textAreaWidth = textWidth + textToBorderMargin * 2;
|
||||
saveStyle();
|
||||
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
|
||||
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.faceName.c_str(),
|
||||
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
|
||||
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut);
|
||||
int titleAreaWidth = textwidth(LPCTSTR(titleText.c_str())) + textToBorderMargin * 2 + closeButtonWidth + buttonMargin;
|
||||
@@ -650,37 +662,15 @@ void Dialog::performDelayedCleanup()
|
||||
|
||||
isCleaning = true;
|
||||
|
||||
auto& c = hWnd.getControls();
|
||||
for (auto& control : c)
|
||||
control->setDirty(true);
|
||||
|
||||
for (auto& control : controls)
|
||||
control->invalidateBackgroundSnapshot();
|
||||
controls.clear();
|
||||
|
||||
// 重置指针
|
||||
closeButton = nullptr;
|
||||
// 释放背景图像资源
|
||||
if (saveBkImage && hasSnap)
|
||||
{
|
||||
restBackground();
|
||||
FlushBatchDraw();
|
||||
invalidateBackgroundSnapshot();
|
||||
}
|
||||
if (!(saveBkImage && hasSnap))
|
||||
{
|
||||
// 没有背景快照:强制一次完整重绘,立即擦掉残影
|
||||
hWnd.pumpResizeIfNeeded(); // 如果正好有尺寸标志,顺便统一收口
|
||||
// 即使没有尺寸变化,也重绘一帧
|
||||
BeginBatchDraw();
|
||||
// 背景
|
||||
if (hWnd.getBkImage() && !hWnd.getBkImageFile().empty())
|
||||
putimage(0, 0, hWnd.getBkImage());
|
||||
else { setbkcolor(hWnd.getBkcolor()); cleardevice(); }
|
||||
// 所有普通控件
|
||||
for (auto& c : hWnd.getControls()) c->draw();
|
||||
// 其他对话框(this 已经 show=false,会早退不绘)
|
||||
EndBatchDraw();
|
||||
FlushBatchDraw();
|
||||
}
|
||||
// 清理阶段不直接写屏;Window 会在安全点统一重绘剩余场景。
|
||||
invalidateBackgroundSnapshot();
|
||||
hWnd.requestFullSceneRedraw();
|
||||
// 重置状态
|
||||
needsInitialization = true;
|
||||
pendingCleanup = false;
|
||||
@@ -8,6 +8,7 @@ namespace
|
||||
// 都可以先把运行态尺寸收口好,而不必等到 draw() 再决定几何。
|
||||
void MeasureLabelText(const std::string& text, const StellarX::ControlText& style, int& width, int& height)
|
||||
{
|
||||
// 使用独立的兼容 DC 测量文本,不依赖当前 EasyX 工作图像,也不会改动屏幕状态。
|
||||
width = 0;
|
||||
height = 0;
|
||||
|
||||
@@ -29,7 +30,7 @@ namespace
|
||||
fontDesc.lfClipPrecision = CLIP_DEFAULT_PRECIS;
|
||||
fontDesc.lfQuality = DEFAULT_QUALITY;
|
||||
fontDesc.lfPitchAndFamily = DEFAULT_PITCH | FF_DONTCARE;
|
||||
lstrcpynA(fontDesc.lfFaceName, style.lpszFace ? style.lpszFace : "微软雅黑", LF_FACESIZE);
|
||||
lstrcpynA(fontDesc.lfFaceName, style.faceName.empty() ? "微软雅黑" : style.faceName.c_str(), LF_FACESIZE);
|
||||
|
||||
HFONT font = CreateFontIndirectA(&fontDesc);
|
||||
HFONT oldFont = nullptr;
|
||||
@@ -104,6 +105,7 @@ void Label::refreshContentDrivenRuntimeGeometry()
|
||||
return;
|
||||
}
|
||||
|
||||
// 只有文本或字体样式发生变化才重新测量,避免每次普通重绘都创建 GDI 资源。
|
||||
int newWidth = 0;
|
||||
int newHeight = 0;
|
||||
MeasureLabelText(text, textStyle, newWidth, newHeight);
|
||||
@@ -136,7 +138,7 @@ void Label::draw()
|
||||
setbkcolor(textBkColor); //设置背景颜色
|
||||
}
|
||||
settextcolor(textStyle.color);
|
||||
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
|
||||
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.faceName.c_str(),
|
||||
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
|
||||
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut); //设置字体样式
|
||||
if ((saveBkX != this->x) || (saveBkY != this->y) || (!hasSnap) || (saveWidth != this->width) || (saveHeight != this->height) || !saveBkImage)
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "SxLog.h"
|
||||
#include <cstdlib>
|
||||
#include <clocale>
|
||||
#include <filesystem>
|
||||
#include <system_error>
|
||||
|
||||
/********************************************************************************
|
||||
* @文件: SxLog.cpp
|
||||
@@ -90,7 +92,7 @@ namespace StellarX
|
||||
// 说明:
|
||||
// - makeTimestampLocal 形如 "2026-01-09 12:34:56"
|
||||
// - 文件名中把 '-' ' ' ':' 替换为 '_',只保留数字与 '_',降低环境差异
|
||||
const std::string ts = SxLogger::makeTimestampLocal();
|
||||
const std::string ts = SxLogger::makeTimestampLocal();
|
||||
std::string safeTs;
|
||||
safeTs.reserve(ts.size());
|
||||
for (char ch : ts)
|
||||
@@ -100,12 +102,32 @@ namespace StellarX
|
||||
}
|
||||
if (safeTs.empty()) safeTs = "rotated";
|
||||
|
||||
const std::string rotated = filePath + "." + safeTs;
|
||||
std::rename(filePath.c_str(), rotated.c_str());
|
||||
// 毫秒和序号共同组成后缀,解决同一秒甚至同一毫秒内多次滚动的冲突。
|
||||
const auto now = std::chrono::system_clock::now();
|
||||
const auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
now.time_since_epoch()).count() % 1000;
|
||||
std::ostringstream suffix;
|
||||
suffix << safeTs << "_" << std::setw(3) << std::setfill('0') << millis;
|
||||
|
||||
// 重新打开新文件
|
||||
// 注意: 这里用 append=false,确保新文件从空开始
|
||||
return open(filePath, false);
|
||||
std::filesystem::path rotated;
|
||||
std::error_code ec;
|
||||
do
|
||||
{
|
||||
rotated = filePath + "." + suffix.str() + "." + std::to_string(rotateSequence++);
|
||||
ec.clear();
|
||||
} while (std::filesystem::exists(rotated, ec) && !ec);
|
||||
|
||||
ec.clear();
|
||||
std::filesystem::rename(filePath, rotated, ec);
|
||||
if (ec)
|
||||
{
|
||||
// 重命名失败时必须保留原文件,并以追加模式恢复写入,绝不能截断。
|
||||
ofs.open(filePath.c_str(), std::ios::out | std::ios::app);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 只有滚动文件已安全落盘后,才创建新的空日志文件。
|
||||
return open(filePath, false);
|
||||
}
|
||||
|
||||
// -------- SxLogger --------
|
||||
@@ -225,23 +247,26 @@ namespace StellarX
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx);
|
||||
|
||||
if (!fileSink) fileSink.reset(new FileSink());
|
||||
fileSink->setRotateBytes(rotateBytes_);
|
||||
|
||||
const bool ok = fileSink->open(path, append);
|
||||
cfg.fileEnabled = ok;
|
||||
cfg.filePath = path;
|
||||
cfg.fileAppend = append;
|
||||
cfg.rotateBytes = rotateBytes_;
|
||||
return ok;
|
||||
if (fileSink) fileSink->flush();
|
||||
auto candidate = std::make_unique<FileSink>();
|
||||
candidate->setRotateBytes(rotateBytes_);
|
||||
if (!candidate->open(path, append))
|
||||
return false;
|
||||
fileSink = std::move(candidate);
|
||||
cfg.fileEnabled = true;
|
||||
cfg.filePath = path;
|
||||
cfg.fileAppend = append;
|
||||
cfg.rotateBytes = rotateBytes_;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 关闭文件输出
|
||||
void SxLogger::disableFile()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx);
|
||||
if (fileSink) fileSink->close();
|
||||
cfg.fileEnabled = false;
|
||||
if (fileSink) fileSink->close();
|
||||
fileSink.reset();
|
||||
cfg.fileEnabled = false;
|
||||
}
|
||||
|
||||
// 获取配置副本
|
||||
@@ -254,10 +279,29 @@ namespace StellarX
|
||||
}
|
||||
|
||||
// 设置配置(整体替换)
|
||||
void SxLogger::setConfig(const SxLogConfig& c)
|
||||
bool SxLogger::setConfig(const SxLogConfig& c)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx);
|
||||
cfg = c;
|
||||
std::unique_ptr<FileSink> candidate;
|
||||
if (c.fileEnabled)
|
||||
{
|
||||
// 先创建候选 sink;任意打开失败都直接返回,保留旧 sink 和旧配置。
|
||||
if (c.filePath.empty())
|
||||
return false;
|
||||
if (fileSink) fileSink->flush();
|
||||
candidate = std::make_unique<FileSink>();
|
||||
candidate->setRotateBytes(c.rotateBytes);
|
||||
if (!candidate->open(c.filePath, c.fileAppend))
|
||||
return false;
|
||||
}
|
||||
|
||||
// 候选 sink 已经可用后才关闭旧 sink,确保配置切换不会出现短暂的无输出状态。
|
||||
if (fileSink)
|
||||
fileSink->close();
|
||||
fileSink = std::move(candidate);
|
||||
cfg = c;
|
||||
cfg.fileEnabled = (fileSink != nullptr);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 级别转字符串
|
||||
@@ -168,30 +168,62 @@ TabControl::~TabControl()
|
||||
|
||||
void TabControl::setX(int x)
|
||||
{
|
||||
const RECT previousCoverage = getManagedRepaintCoverageRect();
|
||||
const bool wasDrawn = hasValidBackgroundSnapshot();
|
||||
if (wasDrawn) discardBackground();
|
||||
applyRuntimeRectDirect(x, y, width, height);
|
||||
refreshRuntimeLayout();
|
||||
onWindowResize();
|
||||
if (wasDrawn)
|
||||
{
|
||||
if (auto* host = getHostWindow()) host->requestManagedRepaint(this, previousCoverage);
|
||||
else requestRepaint(parent);
|
||||
}
|
||||
}
|
||||
|
||||
void TabControl::setY(int y)
|
||||
{
|
||||
const RECT previousCoverage = getManagedRepaintCoverageRect();
|
||||
const bool wasDrawn = hasValidBackgroundSnapshot();
|
||||
if (wasDrawn) discardBackground();
|
||||
applyRuntimeRectDirect(x, y, width, height);
|
||||
refreshRuntimeLayout();
|
||||
onWindowResize();
|
||||
if (wasDrawn)
|
||||
{
|
||||
if (auto* host = getHostWindow()) host->requestManagedRepaint(this, previousCoverage);
|
||||
else requestRepaint(parent);
|
||||
}
|
||||
}
|
||||
|
||||
void TabControl::setWidth(int width)
|
||||
{
|
||||
applyRuntimeRectDirect(x, y, width, height);
|
||||
const RECT previousCoverage = getManagedRepaintCoverageRect();
|
||||
const bool wasDrawn = hasValidBackgroundSnapshot();
|
||||
if (wasDrawn) discardBackground();
|
||||
applyRuntimeRectDirect(x, y, (std::max)(0, width), height);
|
||||
refreshRuntimeLayout();
|
||||
onWindowResize();
|
||||
if (wasDrawn)
|
||||
{
|
||||
if (auto* host = getHostWindow()) host->requestManagedRepaint(this, previousCoverage);
|
||||
else requestRepaint(parent);
|
||||
}
|
||||
}
|
||||
|
||||
void TabControl::setHeight(int height)
|
||||
{
|
||||
applyRuntimeRectDirect(x, y, width, height);
|
||||
const RECT previousCoverage = getManagedRepaintCoverageRect();
|
||||
const bool wasDrawn = hasValidBackgroundSnapshot();
|
||||
if (wasDrawn) discardBackground();
|
||||
applyRuntimeRectDirect(x, y, width, (std::max)(0, height));
|
||||
refreshRuntimeLayout();
|
||||
onWindowResize();
|
||||
if (wasDrawn)
|
||||
{
|
||||
if (auto* host = getHostWindow()) host->requestManagedRepaint(this, previousCoverage);
|
||||
else requestRepaint(parent);
|
||||
}
|
||||
}
|
||||
|
||||
void TabControl::applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect)
|
||||
@@ -205,6 +237,7 @@ void TabControl::applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rec
|
||||
void TabControl::draw()
|
||||
{
|
||||
if (!dirty || !show)return;
|
||||
auto mutationScope = enterMutationScope();
|
||||
// 绘制画布背景和基本形状及其子画布控件
|
||||
Canvas::draw();
|
||||
for (auto& c : controls)
|
||||
@@ -218,8 +251,8 @@ void TabControl::draw()
|
||||
c.first->draw();
|
||||
}
|
||||
|
||||
// 首次绘制时处理默认激活页签
|
||||
if (IsFirstDraw)
|
||||
// 首次绘制时处理默认激活页签;空容器保持待定状态,不能访问最后一个元素。
|
||||
if (IsFirstDraw && !controls.empty())
|
||||
{
|
||||
if (defaultActivation >= 0 && defaultActivation < (int)controls.size())
|
||||
controls[defaultActivation].first->setButtonClick(true);
|
||||
@@ -233,6 +266,7 @@ void TabControl::draw()
|
||||
bool TabControl::handleEvent(const ExMessage& msg)
|
||||
{
|
||||
if (!show)return false;
|
||||
auto mutationScope = enterMutationScope();
|
||||
resetEventVisualChanged();
|
||||
bool consume = false;
|
||||
bool anyVisualChanged = false;
|
||||
@@ -317,9 +351,28 @@ bool TabControl::clearTransientMouseState()
|
||||
return changed;
|
||||
}
|
||||
|
||||
void TabControl::add(std::pair<std::unique_ptr<Button>, std::unique_ptr<Canvas>>&& control)
|
||||
size_t TabControl::addTab(std::unique_ptr<Button> button, std::unique_ptr<Canvas> page)
|
||||
{
|
||||
controls.push_back(std::move(control));
|
||||
if (!button || !page)
|
||||
return static_cast<size_t>(-1);
|
||||
Window* host = getHostWindow();
|
||||
if (mutationScopeDepth > 0 || (host && host->isMutationTransactionActive()))
|
||||
{
|
||||
// 返回“逻辑索引”而不是物理 vector 下标,使回调可以立即向待提交页面添加控件。
|
||||
const size_t index = controls.size() + pendingTabs.size();
|
||||
pendingTabs.emplace_back(std::move(button), std::move(page));
|
||||
dirty = true;
|
||||
return index;
|
||||
}
|
||||
return addTabDirect(std::move(button), std::move(page));
|
||||
}
|
||||
|
||||
size_t TabControl::addTabDirect(std::unique_ptr<Button> button, std::unique_ptr<Canvas> page)
|
||||
{
|
||||
if (!button || !page)
|
||||
return static_cast<size_t>(-1);
|
||||
// 只有在事务提交阶段才真正加入 controls,并在这里一次性建立按钮、页面和回调关系。
|
||||
controls.emplace_back(std::move(button), std::move(page));
|
||||
initTabBar();
|
||||
initTabPage();
|
||||
size_t idx = controls.size() - 1;
|
||||
@@ -338,10 +391,10 @@ void TabControl::add(std::pair<std::unique_ptr<Button>, std::unique_ptr<Canvas>>
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (auto& tab : controls)
|
||||
for (size_t i = 0; i < controls.size(); ++i)
|
||||
{
|
||||
if (tab.first->getButtonText() != controls[idx].first->getButtonText() && tab.first->isClicked())
|
||||
tab.first->setButtonClick(false);
|
||||
if (i != idx && controls[i].first->isClicked())
|
||||
controls[i].first->setButtonClick(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -364,23 +417,68 @@ void TabControl::add(std::pair<std::unique_ptr<Button>, std::unique_ptr<Canvas>>
|
||||
controls[idx].second->setParent(this);
|
||||
controls[idx].second->setLinewidth(canvaslinewidth);
|
||||
controls[idx].second->setIsVisible(false);
|
||||
dirty = true;
|
||||
return idx;
|
||||
}
|
||||
|
||||
void TabControl::add(std::string tabText, std::unique_ptr<Control> control)
|
||||
void TabControl::tick(ULONGLONG nowMs)
|
||||
{
|
||||
control->setDirty(true);
|
||||
if (!show)
|
||||
return;
|
||||
auto mutationScope = enterMutationScope();
|
||||
for (auto& tab : controls)
|
||||
{
|
||||
if (tab.first->getButtonText() == tabText)
|
||||
{
|
||||
control->setParent(tab.second.get());
|
||||
control->setIsVisible(tab.second->IsVisible());
|
||||
tab.second->addControl(std::move(control));
|
||||
break;
|
||||
}
|
||||
if (tab.first && tab.first->IsVisible()) tab.first->tick(nowMs);
|
||||
if (tab.second && tab.second->IsVisible()) tab.second->tick(nowMs);
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<Control> TabControl::tryAddToPage(size_t pageIndex, std::unique_ptr<Control> control)
|
||||
{
|
||||
// 返回非空 unique_ptr 表示索引无效且调用方仍拥有控件;成功路径才会消耗所有权。
|
||||
if (!control)
|
||||
return control;
|
||||
Canvas* page = nullptr;
|
||||
if (pageIndex < controls.size())
|
||||
page = controls[pageIndex].second.get();
|
||||
else
|
||||
{
|
||||
const size_t pendingIndex = pageIndex - controls.size();
|
||||
if (pendingIndex < pendingTabs.size())
|
||||
page = pendingTabs[pendingIndex].second.get();
|
||||
}
|
||||
if (!page)
|
||||
return control;
|
||||
control->setDirty(true);
|
||||
control->setIsVisible(page->IsVisible());
|
||||
page->addControl(std::move(control));
|
||||
dirty = true;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void TabControl::flushPendingMutations()
|
||||
{
|
||||
// 先提交 Canvas 自身的页内控件,再提交完整页签;两层队列都在同一外层收口完成。
|
||||
Canvas::flushPendingMutations();
|
||||
if (!pendingTabs.empty())
|
||||
{
|
||||
auto tabs = std::move(pendingTabs);
|
||||
pendingTabs.clear();
|
||||
for (auto& tab : tabs)
|
||||
addTabDirect(std::move(tab.first), std::move(tab.second));
|
||||
}
|
||||
for (auto& tab : controls)
|
||||
if (tab.second) tab.second->flushPendingMutations();
|
||||
}
|
||||
|
||||
std::unique_ptr<Control> TabControl::tryAddToPage(const std::string& tabText, std::unique_ptr<Control> control)
|
||||
{
|
||||
const int idx = indexOf(tabText);
|
||||
if (idx < 0)
|
||||
return control;
|
||||
return tryAddToPage(static_cast<size_t>(idx), std::move(control));
|
||||
}
|
||||
|
||||
void TabControl::setTabPlacement(StellarX::TabPlacement placement)
|
||||
{
|
||||
this->tabPlacement = placement;
|
||||
@@ -446,22 +544,30 @@ int TabControl::getActiveIndex() const
|
||||
return -1;
|
||||
}
|
||||
|
||||
void TabControl::setActiveIndex(int idx)
|
||||
bool TabControl::setActiveIndex(int idx)
|
||||
{
|
||||
// 首次绘制前仅记录待激活索引,真正页签尚未加入时也不访问 controls。
|
||||
if (idx < 0)
|
||||
return false;
|
||||
if (IsFirstDraw)
|
||||
{
|
||||
defaultActivation = idx;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 外部重复激活“已经处于激活状态”的页签时,不应再把整条 onToggleOn 链重跑一遍。
|
||||
// 否则当前可见页面会重复执行 onWindowResize()/setIsVisible(true),
|
||||
// 对页内像 Table 这种会越出页面边界绘制的控件,容易把快照链再次扰乱,留下残影。
|
||||
if (idx >= 0 && idx < controls.size())
|
||||
if (idx >= 0 && idx < static_cast<int>(controls.size()))
|
||||
{
|
||||
if (getActiveIndex() == idx)
|
||||
return;
|
||||
return true;
|
||||
controls[idx].first->setButtonClick(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int TabControl::count() const
|
||||
@@ -1,8 +1,43 @@
|
||||
#include "Table.h"
|
||||
#include "SxLog.h"
|
||||
#include "Window.h"
|
||||
#include <algorithm>
|
||||
|
||||
namespace
|
||||
{
|
||||
int TableMbcCharLen(const std::string& text, size_t index)
|
||||
{
|
||||
const unsigned char lead = static_cast<unsigned char>(text[index]);
|
||||
if (lead <= 0x7f) return 1;
|
||||
if (lead >= 0x81 && lead <= 0xfe && index + 1 < text.size())
|
||||
{
|
||||
const unsigned char trail = static_cast<unsigned char>(text[index + 1]);
|
||||
if (trail >= 0x40 && trail <= 0xfe && trail != 0x7f) return 2;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::string EllipsizeTableText(const std::string& text, int maxWidth)
|
||||
{
|
||||
// 逐个 MBCS 字符试探可用宽度,保证省略号不会把双字节字符拆成半个字节。
|
||||
if (maxWidth <= 0) return "";
|
||||
if (textwidth(text.c_str()) <= maxWidth) return text;
|
||||
const std::string ellipsis = "...";
|
||||
const int ellipsisWidth = textwidth(ellipsis.c_str());
|
||||
if (ellipsisWidth > maxWidth) return "";
|
||||
size_t index = 0;
|
||||
size_t lastFit = 0;
|
||||
while (index < text.size())
|
||||
{
|
||||
const size_t next = index + static_cast<size_t>(TableMbcCharLen(text, index));
|
||||
if (next > text.size()) break;
|
||||
if (textwidth(text.substr(0, next).c_str()) > maxWidth - ellipsisWidth) break;
|
||||
lastFit = next;
|
||||
index = next;
|
||||
}
|
||||
return text.substr(0, lastFit) + ellipsis;
|
||||
}
|
||||
|
||||
std::vector<std::string> NormalizeTableRow(std::vector<std::string> row, size_t headerCount)
|
||||
{
|
||||
if (headerCount == 0)
|
||||
@@ -24,6 +59,12 @@ namespace
|
||||
const int total = static_cast<int>((rowCount + static_cast<size_t>(rowsPerPage) - 1) / static_cast<size_t>(rowsPerPage));
|
||||
return total > 0 ? total : 1;
|
||||
}
|
||||
|
||||
std::string BuildPageNumberText(int currentPage, int totalPages)
|
||||
{
|
||||
return std::string(TABLE_STR_PAGE_PREFIX) + std::to_string(currentPage) +
|
||||
TABLE_STR_PAGE_MID + std::to_string(totalPages) + TABLE_STR_PAGE_SUFFIX;
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制表格的当前页
|
||||
@@ -49,7 +90,9 @@ void Table::drawTable()
|
||||
{
|
||||
uX = dX + colWidths.at(j) + TABLE_COL_GAP;
|
||||
fillrectangle(dX, dY, uX, uY);
|
||||
outtextxy(dX + TABLE_PAD_X, dY + TABLE_PAD_Y, LPCTSTR(data[i][j].c_str()));
|
||||
const std::string display = EllipsizeTableText(data[i][j],
|
||||
(std::max)(0, colWidths.at(j) - TABLE_PAD_X * 2));
|
||||
outtextxy(dX + TABLE_PAD_X, dY + TABLE_PAD_Y, LPCTSTR(display.c_str()));
|
||||
dX += colWidths.at(j) + TABLE_COL_GAP;
|
||||
}
|
||||
dX = x + border;
|
||||
@@ -73,7 +116,9 @@ void Table::drawHeader()
|
||||
{
|
||||
uX = dX + colWidths.at(i) + TABLE_COL_GAP; // 注意这里是 +20,和表体一致
|
||||
fillrectangle(dX, dY, uX, uY);
|
||||
outtextxy(dX + TABLE_PAD_X, dY + TABLE_PAD_Y, LPCTSTR(headers[i].c_str()));
|
||||
const std::string display = EllipsizeTableText(headers[i],
|
||||
(std::max)(0, colWidths.at(i) - TABLE_PAD_X * 2));
|
||||
outtextxy(dX + TABLE_PAD_X, dY + TABLE_PAD_Y, LPCTSTR(display.c_str()));
|
||||
dX += colWidths.at(i) + TABLE_COL_GAP; // 列间距 20
|
||||
}
|
||||
}
|
||||
@@ -123,6 +168,7 @@ void Table::initTextWaH()
|
||||
maxLineH = h;
|
||||
if (maxLineH == 0)
|
||||
maxLineH = textheight(LPCTSTR("A"));
|
||||
std::fill(lineHeights.begin(), lineHeights.end(), maxLineH);
|
||||
if (rowsPerPage < 1)
|
||||
rowsPerPage = 1;
|
||||
|
||||
@@ -254,10 +300,6 @@ void Table::initPageNum()
|
||||
const int headerH = baseH + TABLE_HEADER_EXTRA;
|
||||
const int rowsH = baseH * rowsPerPage + rowsPerPage * TABLE_ROW_EXTRA;
|
||||
|
||||
// 内容宽度 = sum(colWidths + 20);initTextWaH() 已把 this->width += 2*border
|
||||
// 因此 contentW = this->width - 2*border 更稳妥
|
||||
const int contentW = this->width - (border << 1);
|
||||
|
||||
// 页脚顶部位置(表头 + 可视数据区 之后)
|
||||
pY = y + border + headerH + rowsH + TABLE_FOOTER_BLANK; // +8 顶部留白
|
||||
|
||||
@@ -283,11 +325,7 @@ void Table::initPageNum()
|
||||
|
||||
void Table::drawPageNum()
|
||||
{
|
||||
pageNumtext = "第";
|
||||
pageNumtext += std::to_string(currentPage);
|
||||
pageNumtext += "页/共";
|
||||
pageNumtext += std::to_string(totalPages);
|
||||
pageNumtext += "页";
|
||||
pageNumtext = BuildPageNumberText(currentPage, totalPages);
|
||||
if (nullptr == pageNum || isNeedButtonAndPageNum)
|
||||
initPageNum();
|
||||
pageNum->setText(pageNumtext);
|
||||
@@ -320,50 +358,74 @@ void Table::drawButton()
|
||||
|
||||
void Table::setX(int x)
|
||||
{
|
||||
const RECT previousCoverage = getManagedRepaintCoverageRect();
|
||||
const bool wasDrawn = hasValidBackgroundSnapshot();
|
||||
if (wasDrawn) discardBackground();
|
||||
this->x = x;
|
||||
isNeedButtonAndPageNum = true;
|
||||
dirty = true;
|
||||
if (wasDrawn)
|
||||
{
|
||||
if (auto* host = getHostWindow()) host->requestManagedRepaint(this, previousCoverage);
|
||||
else requestRepaint(parent);
|
||||
}
|
||||
}
|
||||
|
||||
void Table::setY(int y)
|
||||
{
|
||||
const RECT previousCoverage = getManagedRepaintCoverageRect();
|
||||
const bool wasDrawn = hasValidBackgroundSnapshot();
|
||||
if (wasDrawn) discardBackground();
|
||||
this->y = y;
|
||||
isNeedButtonAndPageNum = true;
|
||||
dirty = true;
|
||||
if (wasDrawn)
|
||||
{
|
||||
if (auto* host = getHostWindow()) host->requestManagedRepaint(this, previousCoverage);
|
||||
else requestRepaint(parent);
|
||||
}
|
||||
}
|
||||
|
||||
void Table::setWidth(int width)
|
||||
{
|
||||
// 调整列宽以匹配新的表格总宽度。不修改 localWidth,避免累计误差。
|
||||
// 当 width 与当前 width 不同时,根据差值平均分配到各列,余数依次累加/扣减。
|
||||
const RECT previousCoverage = getManagedRepaintCoverageRect();
|
||||
const bool wasDrawn = hasValidBackgroundSnapshot();
|
||||
if (wasDrawn) discardBackground();
|
||||
// 运行态总宽必须和实际列宽严格一致,避免绘制超出快照与 coverage。
|
||||
// 同时考虑表体和分页栏的最小宽度,随后把余数分配到前几列,使列宽总和严格闭合。
|
||||
const int ncols = static_cast<int>(colWidths.size());
|
||||
if (ncols <= 0) {
|
||||
this->width = width;
|
||||
this->width = (std::max)(0, width);
|
||||
isNeedButtonAndPageNum = true;
|
||||
dirty = true;
|
||||
if (wasDrawn)
|
||||
{
|
||||
if (auto* host = getHostWindow()) host->requestManagedRepaint(this, previousCoverage);
|
||||
else requestRepaint(parent);
|
||||
}
|
||||
return;
|
||||
}
|
||||
int diff = width - this->width;
|
||||
// 基础增量:整除部分
|
||||
int baseChange = diff / ncols;
|
||||
int remainder = diff % ncols;
|
||||
for (int i = 0; i < ncols; ++i) {
|
||||
int change = baseChange;
|
||||
if (remainder > 0) {
|
||||
change += 1;
|
||||
remainder -= 1;
|
||||
}
|
||||
else if (remainder < 0) {
|
||||
change -= 1;
|
||||
remainder += 1;
|
||||
}
|
||||
int newWidth = colWidths[i] + change;
|
||||
// 限制最小宽度为 1,防止出现负值
|
||||
if (newWidth < 1) newWidth = 1;
|
||||
colWidths[i] = newWidth;
|
||||
}
|
||||
this->width = width;
|
||||
const int border = (std::max)(0, tableBorderWidth);
|
||||
const int minColumnWidth = TABLE_PAD_X * 2 + 1;
|
||||
const int contentMinWidth = (border << 1) + ncols * (minColumnWidth + TABLE_COL_GAP);
|
||||
const int footerMinWidth = (border << 1) + textwidth(TABLE_STR_PREV) + textwidth(TABLE_STR_NEXT) +
|
||||
textwidth(pageNumtext.c_str()) + TABLE_BTN_PAD_H * 4 + TABLE_BTN_GAP * 2;
|
||||
const int minWidth = isShowPageButton ? (std::max)(contentMinWidth, footerMinWidth) : contentMinWidth;
|
||||
const int targetWidth = (std::max)(width, minWidth);
|
||||
const int targetColumnsWidth = targetWidth - (border << 1) - ncols * TABLE_COL_GAP;
|
||||
const int baseWidth = targetColumnsWidth / ncols;
|
||||
int remainder = targetColumnsWidth % ncols;
|
||||
for (int i = 0; i < ncols; ++i)
|
||||
colWidths[i] = baseWidth + (remainder-- > 0 ? 1 : 0);
|
||||
this->width = targetWidth;
|
||||
// 需要重新布局页脚元素
|
||||
isNeedButtonAndPageNum = true;
|
||||
dirty = true;
|
||||
if (wasDrawn)
|
||||
{
|
||||
if (auto* host = getHostWindow()) host->requestManagedRepaint(this, previousCoverage);
|
||||
else requestRepaint(parent);
|
||||
}
|
||||
}
|
||||
|
||||
void Table::setHeight(int height)
|
||||
@@ -419,7 +481,7 @@ void Table::draw()
|
||||
// 设置表格样式
|
||||
setfillcolor(tableBkClor);
|
||||
setlinecolor(tableBorderClor);
|
||||
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
|
||||
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.faceName.c_str(),
|
||||
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
|
||||
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut);
|
||||
settextcolor(textStyle.color);
|
||||
@@ -440,7 +502,7 @@ void Table::draw()
|
||||
// 设置表格样式
|
||||
setfillcolor(tableBkClor);
|
||||
setlinecolor(tableBorderClor);
|
||||
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
|
||||
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.faceName.c_str(),
|
||||
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
|
||||
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut);
|
||||
settextcolor(textStyle.color);
|
||||
@@ -448,12 +510,29 @@ void Table::draw()
|
||||
setfillstyle((int)tableFillMode);
|
||||
setbkmode(TRANSPARENT);
|
||||
|
||||
if (isNeedButtonAndPageNum)
|
||||
{
|
||||
// 数据变化可能同时改变表格宽度、页码位置和两个分页按钮的位置。
|
||||
// 必须在绘制新内容前完成三个内部控件的搬移,让 setX/setY 先回贴旧快照;
|
||||
// 否则后移动的分页按钮会用旧背景覆盖已经画好的新页码。
|
||||
pageNumtext = BuildPageNumberText(currentPage, totalPages);
|
||||
initPageNum();
|
||||
pageNum->textStyle = this->textStyle;
|
||||
pageNum->setText(pageNumtext);
|
||||
pageNum->setDirty(true);
|
||||
|
||||
if (isShowPageButton)
|
||||
initButton();
|
||||
else
|
||||
isNeedButtonAndPageNum = false;
|
||||
}
|
||||
|
||||
if (isNeedDrawHeaders)
|
||||
{
|
||||
// 重新设置表格样式
|
||||
setfillcolor(tableBkClor);
|
||||
setlinecolor(tableBorderClor);
|
||||
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
|
||||
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.faceName.c_str(),
|
||||
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
|
||||
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut);
|
||||
settextcolor(textStyle.color);
|
||||
@@ -584,6 +663,7 @@ bool Table::clearTransientMouseState()
|
||||
|
||||
RECT Table::getManagedRepaintCoverageRect() const
|
||||
{
|
||||
// coverage 包含表体、页码和分页按钮的实际绘制范围,而不只包含表格本体。
|
||||
RECT coverage = getBoundsRect();
|
||||
|
||||
if (pageNum && pageNum->IsVisible())
|
||||
@@ -1,26 +1,10 @@
|
||||
// TextBox.cpp
|
||||
#include "TextBox.h"
|
||||
#include "SxLog.h"
|
||||
#include "SxTextInternal.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
int SxTextBoxMbcCharLen(const std::string& s, size_t i)
|
||||
{
|
||||
unsigned char b = static_cast<unsigned char>(s[i]);
|
||||
if (b <= 0x7F)
|
||||
return 1;
|
||||
|
||||
if (b >= 0x81 && b <= 0xFE && i + 1 < s.size())
|
||||
{
|
||||
unsigned char b2 = static_cast<unsigned char>(s[i + 1]);
|
||||
if (b2 >= 0x40 && b2 <= 0xFE && b2 != 0x7F)
|
||||
return 2;
|
||||
}
|
||||
|
||||
// 非法或不完整字节按单字节容错,避免死循环。
|
||||
return 1;
|
||||
}
|
||||
|
||||
void SxTextBoxTrimTrailingSpaces(std::string& s)
|
||||
{
|
||||
while (!s.empty() && s.back() == ' ')
|
||||
@@ -39,12 +23,12 @@ namespace
|
||||
|
||||
std::string SxTextBoxTruncateByMbcBoundary(const std::string& text, size_t maxBytes)
|
||||
{
|
||||
// maxBytes 保持公开 API 的字节长度语义,但截断点必须落在完整 MBCS 字符边界。
|
||||
size_t i = 0;
|
||||
size_t lastSafe = 0;
|
||||
while (i < text.size())
|
||||
{
|
||||
const int charLen = SxTextBoxMbcCharLen(text, i);
|
||||
const size_t next = i + static_cast<size_t>(charLen);
|
||||
const size_t next = i + StellarX::Internal::mbcCharLength(text, i);
|
||||
if (next > maxBytes || next > text.size())
|
||||
break;
|
||||
lastSafe = next;
|
||||
@@ -55,6 +39,7 @@ namespace
|
||||
|
||||
std::string SxTextBoxEllipsizeMbc(const std::string& text, int maxWidth)
|
||||
{
|
||||
// 先按像素宽度试探完整字符,再追加省略号,避免中文文本被切成非法字节序列。
|
||||
if (maxWidth <= 0)
|
||||
return "";
|
||||
if (textwidth(LPCTSTR(text.c_str())) <= maxWidth)
|
||||
@@ -75,8 +60,7 @@ namespace
|
||||
size_t lastFit = 0;
|
||||
while (i < text.size())
|
||||
{
|
||||
const int charLen = SxTextBoxMbcCharLen(text, i);
|
||||
const size_t next = i + static_cast<size_t>(charLen);
|
||||
const size_t next = i + StellarX::Internal::mbcCharLength(text, i);
|
||||
if (next > text.size())
|
||||
break;
|
||||
|
||||
@@ -124,7 +108,7 @@ void TextBox::draw()
|
||||
drawStyle.nHeight = height;
|
||||
if (drawStyle.nWidth > width)
|
||||
drawStyle.nWidth = width;
|
||||
settextstyle(drawStyle.nHeight, drawStyle.nWidth, drawStyle.lpszFace,
|
||||
settextstyle(drawStyle.nHeight, drawStyle.nWidth, drawStyle.faceName.c_str(),
|
||||
drawStyle.nEscapement, drawStyle.nOrientation, drawStyle.nWeight,
|
||||
drawStyle.bItalic, drawStyle.bUnderline, drawStyle.bStrikeOut);
|
||||
|
||||
@@ -138,8 +122,7 @@ void TextBox::draw()
|
||||
|
||||
if (StellarX::TextBoxmode::PASSWORD_MODE == mode)
|
||||
{
|
||||
for (size_t i = 0; i < text.size(); ++i)
|
||||
pwdText += '*';
|
||||
pwdText = StellarX::Internal::makePasswordMask(text);
|
||||
displayText = pwdText;
|
||||
}
|
||||
else
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "Window.h"
|
||||
#include "Dialog.h"
|
||||
#include"SxLog.h"
|
||||
#include "SxEventLoopInternal.h"
|
||||
#include <easyx.h>
|
||||
#include <algorithm>
|
||||
// 可能频繁出现且对调试信息干扰较大的消息(例如鼠标移动),
|
||||
@@ -51,6 +52,97 @@ bool Window::isManagedDispatchActive() const
|
||||
return managedDispatchActive;
|
||||
}
|
||||
|
||||
void Window::beginMutationScope()
|
||||
{
|
||||
// Window 是顶层 mutation 边界;嵌套事务只增加深度,不立即提交任何结构变更。
|
||||
++mutationScopeDepth;
|
||||
}
|
||||
|
||||
void Window::endMutationScope()
|
||||
{
|
||||
// 只有最外层事务结束后才允许提交队列,保证事件遍历和回调链中的对象地址稳定。
|
||||
if (mutationScopeDepth > 0)
|
||||
--mutationScopeDepth;
|
||||
if (mutationScopeDepth == 0 && !managedDispatchActive)
|
||||
flushPendingMutationsInTree();
|
||||
}
|
||||
|
||||
bool Window::isMutationTransactionActive() const
|
||||
{
|
||||
// applyingControlMutations 也视作事务,避免提交队列时因回调递归修改同一容器。
|
||||
return managedDispatchActive || mutationScopeDepth > 0 || applyingControlMutations;
|
||||
}
|
||||
|
||||
void Window::requestFullSceneRedraw()
|
||||
{
|
||||
// 非局部变化只置位,不在调用点立即绘制;主循环安全点会统一消费该请求。
|
||||
fullSceneRedrawRequested = true;
|
||||
}
|
||||
|
||||
void Window::flushPendingControlMutations()
|
||||
{
|
||||
// Window 是顶层所有权边界;这里只在最外层事务结束后修改 controls,保证回调遍历安全。
|
||||
if (applyingControlMutations || pendingControlMutations.empty())
|
||||
return;
|
||||
applyingControlMutations = true;
|
||||
bool changed = false;
|
||||
for (auto& mutation : pendingControlMutations)
|
||||
{
|
||||
switch (mutation.kind)
|
||||
{
|
||||
case ControlMutationKind::Add:
|
||||
if (mutation.control)
|
||||
{
|
||||
mutation.control->setHostWindow(this);
|
||||
controls.push_back(std::move(mutation.control));
|
||||
changed = true;
|
||||
}
|
||||
break;
|
||||
case ControlMutationKind::Remove:
|
||||
{
|
||||
auto it = std::find_if(controls.begin(), controls.end(), [&](const auto& item)
|
||||
{ return item && item->getHandle() == mutation.handle; });
|
||||
if (it != controls.end())
|
||||
{
|
||||
controls.erase(it);
|
||||
changed = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ControlMutationKind::Clear:
|
||||
if (!controls.empty())
|
||||
{
|
||||
controls.clear();
|
||||
changed = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
pendingControlMutations.clear();
|
||||
applyingControlMutations = false;
|
||||
if (changed)
|
||||
{
|
||||
// 顶层结构变化会影响 z-order 和所有背景快照,因此不能只重画单个旧 root。
|
||||
clearManagedRepaintState();
|
||||
fullSceneRedrawRequested = true;
|
||||
}
|
||||
}
|
||||
|
||||
void Window::flushPendingMutationsInTree()
|
||||
{
|
||||
flushPendingControlMutations();
|
||||
for (auto& control : controls)
|
||||
if (control) control->flushPendingMutations();
|
||||
for (auto& dialog : dialogs)
|
||||
if (dialog) dialog->flushPendingMutations();
|
||||
}
|
||||
|
||||
void Window::markAllControlsDirty()
|
||||
{
|
||||
for (auto& control : controls)
|
||||
if (control) control->setDirty(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* requestManagedRepaint(source)
|
||||
* 作用:在“事件分发期”登记一笔托管重绘请求,而不是立即绘制。
|
||||
@@ -171,6 +263,8 @@ void Window::drawWindowBackground()
|
||||
|
||||
void Window::redrawScene(bool forceControlsDirty, bool forceDialogsDirty)
|
||||
{
|
||||
// 全场景重绘是 resize、对话框层级变化和结构提交的保守收口路径。
|
||||
beginMutationScope();
|
||||
drawWindowBackground();
|
||||
|
||||
for (auto& c : controls)
|
||||
@@ -185,6 +279,7 @@ void Window::redrawScene(bool forceControlsDirty, bool forceDialogsDirty)
|
||||
d->setDirty(true);
|
||||
d->draw();
|
||||
}
|
||||
endMutationScope();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -571,6 +666,7 @@ Window::Window(int w, int h, int mode, COLORREF bk, std::string title)
|
||||
Window::~Window()
|
||||
{
|
||||
// 先销毁控件树,再关闭图形环境,避免控件析构时访问已关闭的 EasyX 上下文。
|
||||
pendingControlMutations.clear();
|
||||
dialogs.clear();
|
||||
controls.clear();
|
||||
background.reset();
|
||||
@@ -786,22 +882,18 @@ void Window::draw(std::string imagePath)
|
||||
int Window::runEventLoop()
|
||||
{
|
||||
ExMessage msg;
|
||||
bool running = true;
|
||||
|
||||
// 说明:统一使用 needResizeDirty 作为“收口重绘”的唯一标志位
|
||||
// 不再引入额外 pendingResize 等状态,避免分叉导致状态不一致。
|
||||
while (running)
|
||||
while (true)
|
||||
{
|
||||
|
||||
bool consume = false; // 事件是否被消费的标志(用于输入事件分发)
|
||||
|
||||
if (peekmessage(&msg, EX_MOUSE | EX_KEY | EX_WINDOW, true))
|
||||
{
|
||||
// drainMessageBatch 将一轮消息限制在固定上限;批次结束后统一处理 resize、tick 和重绘。
|
||||
const auto batch = StellarX::Internal::drainMessageBatch(msg,
|
||||
[](ExMessage& next) { return peekmessage(&next, EX_MOUSE | EX_KEY | EX_WINDOW, true); },
|
||||
[&](const ExMessage&) {
|
||||
bool consume = false; // 每条消息独立判断,不能把上一条消息的消费状态带入下一条。
|
||||
if (msg.message == WM_CLOSE)
|
||||
{
|
||||
running = false;
|
||||
return 0;
|
||||
}
|
||||
return false;
|
||||
|
||||
// 保险:如果 EX_WINDOW 转译了 GETMINMAXINFO,同样按最小客户区折算处理
|
||||
if (msg.message == WM_GETMINMAXINFO)
|
||||
@@ -813,7 +905,7 @@ int Window::runEventLoop()
|
||||
AdjustWindowRectEx(&rc, style, FALSE, ex);
|
||||
mmi->ptMinTrackSize.x = rc.right - rc.left;
|
||||
mmi->ptMinTrackSize.y = rc.bottom - rc.top;
|
||||
continue;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 关键点⑥:WM_SIZE 只记录新尺寸;若非拉伸阶段则立即置位 needResizeDirty
|
||||
@@ -835,7 +927,7 @@ int Window::runEventLoop()
|
||||
|
||||
}
|
||||
}
|
||||
continue;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 输入优先:先给顶层“非模态对话框”,再传给普通控件。
|
||||
@@ -908,11 +1000,24 @@ int Window::runEventLoop()
|
||||
<< SX_T(" id=", " id=") << firstConsumer->getId();
|
||||
}
|
||||
managedDispatchActive = false;
|
||||
}
|
||||
flushPendingMutationsInTree();
|
||||
return true;
|
||||
});
|
||||
if (batch.stopped)
|
||||
return 0;
|
||||
|
||||
const ULONGLONG nowMs = GetTickCount64();
|
||||
// tick 独立于鼠标消息推进 Tooltip 等延时状态,仍放在事务内避免回调立即改 vector。
|
||||
beginMutationScope();
|
||||
for (auto& control : controls)
|
||||
if (control && control->IsVisible()) control->tick(nowMs);
|
||||
for (auto& dialog : dialogs)
|
||||
if (dialog && dialog->IsVisible()) dialog->tick(nowMs);
|
||||
endMutationScope();
|
||||
|
||||
// 对话框打开/关闭属于全局层级变化:这里仍然使用整场景重绘兜底,
|
||||
// 并在结束后清空本轮托管重绘登记,避免旧的 root 请求延后提交。
|
||||
bool needredraw = false;
|
||||
bool needredraw = fullSceneRedrawRequested;
|
||||
if(dialogOpen)
|
||||
{
|
||||
for (auto& d : dialogs)
|
||||
@@ -939,6 +1044,7 @@ int Window::runEventLoop()
|
||||
EndBatchDraw();
|
||||
needredraw = false;
|
||||
dialogOpen = false;
|
||||
fullSceneRedrawRequested = false;
|
||||
clearManagedRepaintState();
|
||||
|
||||
}
|
||||
@@ -1059,11 +1165,10 @@ int Window::runEventLoop()
|
||||
|
||||
sweepClosedDialogs();
|
||||
|
||||
// 轻微睡眠,削峰填谷(不阻塞拖拽体验)
|
||||
Sleep(10);
|
||||
// 只在真正空闲时短暂让出 CPU;有消息的批次不得人为限速。
|
||||
if (batch.shouldSleep())
|
||||
Sleep(1);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------- 其余接口 ----------------
|
||||
@@ -1099,11 +1204,76 @@ void Window::setHeadline(std::string title)
|
||||
SetWindowText(hWnd, headline.c_str());
|
||||
}
|
||||
|
||||
void Window::addControl(std::unique_ptr<Control> control)
|
||||
StellarX::ControlHandle Window::addControl(std::unique_ptr<Control> control)
|
||||
{
|
||||
if (!control)
|
||||
return {};
|
||||
const StellarX::ControlHandle handle = control->getHandle();
|
||||
// 回调或绘制阶段只排队所有权变更;非事务调用则立即提交,保持旧的同步使用体验。
|
||||
if (isMutationTransactionActive())
|
||||
{
|
||||
PendingControlMutation mutation;
|
||||
mutation.kind = ControlMutationKind::Add;
|
||||
mutation.handle = handle;
|
||||
mutation.control = std::move(control);
|
||||
pendingControlMutations.push_back(std::move(mutation));
|
||||
return handle;
|
||||
}
|
||||
// 新增控件:仅加入管理容器,具体绘制在 draw()/收口时统一进行
|
||||
control->setHostWindow(this);
|
||||
controls.push_back(std::move(control));
|
||||
return handle;
|
||||
}
|
||||
|
||||
bool Window::removeControl(StellarX::ControlHandle handle)
|
||||
{
|
||||
if (!handle)
|
||||
return false;
|
||||
bool exists = std::find_if(controls.begin(), controls.end(), [&](const auto& item)
|
||||
{ return item && item->getHandle() == handle; }) != controls.end();
|
||||
for (const auto& mutation : pendingControlMutations)
|
||||
{
|
||||
if (mutation.kind == ControlMutationKind::Clear)
|
||||
exists = false;
|
||||
else if (mutation.handle == handle)
|
||||
exists = mutation.kind == ControlMutationKind::Add;
|
||||
}
|
||||
if (!exists)
|
||||
return false;
|
||||
pendingControlMutations.push_back({ ControlMutationKind::Remove, handle, nullptr });
|
||||
if (!isMutationTransactionActive())
|
||||
flushPendingMutationsInTree();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Window::clearControls()
|
||||
{
|
||||
pendingControlMutations.push_back({ ControlMutationKind::Clear, {}, nullptr });
|
||||
if (!isMutationTransactionActive())
|
||||
flushPendingMutationsInTree();
|
||||
}
|
||||
|
||||
Control* Window::findControl(StellarX::ControlHandle handle)
|
||||
{
|
||||
auto it = std::find_if(controls.begin(), controls.end(), [&](const auto& item)
|
||||
{ return item && item->getHandle() == handle; });
|
||||
return it == controls.end() ? nullptr : it->get();
|
||||
}
|
||||
|
||||
const Control* Window::findControl(StellarX::ControlHandle handle) const
|
||||
{
|
||||
auto it = std::find_if(controls.begin(), controls.end(), [&](const auto& item)
|
||||
{ return item && item->getHandle() == handle; });
|
||||
return it == controls.end() ? nullptr : it->get();
|
||||
}
|
||||
|
||||
std::vector<StellarX::ControlHandle> Window::getControlHandles() const
|
||||
{
|
||||
std::vector<StellarX::ControlHandle> handles;
|
||||
handles.reserve(controls.size());
|
||||
for (const auto& control : controls)
|
||||
if (control) handles.push_back(control->getHandle());
|
||||
return handles;
|
||||
}
|
||||
|
||||
void Window::addDialog(std::unique_ptr<Control> dlg)
|
||||
@@ -1174,11 +1344,6 @@ std::string Window::getBkImageFile() const
|
||||
return bkImageFile;
|
||||
}
|
||||
|
||||
std::vector<std::unique_ptr<Control>>& Window::getControls()
|
||||
{
|
||||
return controls;
|
||||
}
|
||||
|
||||
void Window::pumpResizeIfNeeded()
|
||||
{
|
||||
if (!needResizeDirty) return;
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
// 窗口实现和测试共用的内部事件循环工具。
|
||||
// 本文件不属于 StellarX 公共 API。
|
||||
namespace StellarX::Internal
|
||||
{
|
||||
// 限制单轮处理量,避免持续的消息流长期占用循环而导致重绘得不到执行。
|
||||
constexpr std::size_t MessageBatchLimit = 128;
|
||||
|
||||
struct MessageBatchResult
|
||||
{
|
||||
// 本批次成功获取并交给处理器的消息数量。
|
||||
std::size_t processed = 0;
|
||||
// 处理器请求提前停止时为 true,例如处理 WM_CLOSE 后结束循环。
|
||||
bool stopped = false;
|
||||
|
||||
// 空批次表示当前轮次空闲,可以进行短暂休眠。
|
||||
bool shouldSleep() const noexcept { return processed == 0; }
|
||||
};
|
||||
|
||||
// 获取并处理最多 `limit` 条消息;处理器返回 false 时提前停止。
|
||||
// 没有可用消息时,获取器必须返回 false。每次获取到消息都复用同一个
|
||||
// message 对象,从而不要求本工具绑定某一种具体的消息队列类型。
|
||||
template <typename Message, typename Fetch, typename Handle>
|
||||
MessageBatchResult drainMessageBatch(Message& message, Fetch&& fetch, Handle&& handle,
|
||||
std::size_t limit = MessageBatchLimit)
|
||||
{
|
||||
MessageBatchResult result;
|
||||
while (result.processed < limit && fetch(message))
|
||||
{
|
||||
++result.processed;
|
||||
if (!handle(message))
|
||||
{
|
||||
result.stopped = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
|
||||
// 文本控件使用的内部 MBCS 辅助工具。
|
||||
// 按项目约定的 GBK 风格字节规则处理,并且不作为公共 API 暴露。
|
||||
namespace StellarX::Internal
|
||||
{
|
||||
// 返回从 `index` 开始的逻辑字符所占的字节数。
|
||||
// ASCII 字符占一个字节,合法的首字节和尾字节组合占两个字节。
|
||||
// 非法或不完整的输入按一个字节处理,确保调用方始终能够向前推进。
|
||||
inline std::size_t mbcCharLength(const std::string& text, std::size_t index) noexcept
|
||||
{
|
||||
if (index >= text.size())
|
||||
return 0;
|
||||
|
||||
const auto first = static_cast<unsigned char>(text[index]);
|
||||
if (first <= 0x7F)
|
||||
return 1;
|
||||
|
||||
if (first >= 0x81 && first <= 0xFE && index + 1 < text.size())
|
||||
{
|
||||
const auto second = static_cast<unsigned char>(text[index + 1]);
|
||||
if (second >= 0x40 && second <= 0xFE && second != 0x7F)
|
||||
return 2;
|
||||
}
|
||||
|
||||
// 非法或不完整的输入消耗一个字节,确保调用方始终能够向前推进。
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 为每个逻辑 MBCS 字符生成一个掩码字符,同时保留原始字节数语义,
|
||||
// 用原文本的字节长度为结果预留容量。
|
||||
inline std::string makePasswordMask(const std::string& text)
|
||||
{
|
||||
std::string result;
|
||||
result.reserve(text.size());
|
||||
for (std::size_t index = 0; index < text.size();)
|
||||
{
|
||||
result.push_back('*');
|
||||
index += mbcCharLength(text, index);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|Win32"><Configuration>Debug</Configuration><Platform>Win32</Platform></ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|Win32"><Configuration>Release</Configuration><Platform>Win32</Platform></ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug|x64"><Configuration>Debug</Configuration><Platform>x64</Platform></ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64"><Configuration>Release</Configuration><Platform>x64</Platform></ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>17.0</VCProjectVersion>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<ProjectGuid>{8779C678-AD5B-4828-A4A8-57D4C4254733}</ProjectGuid>
|
||||
<RootNamespace>StellarXTests</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
<ProjectSubType>NativeUnitTestProject</ProjectSubType>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<UseDebugLibraries Condition="'$(Configuration)'=='Debug'">true</UseDebugLibraries>
|
||||
<UseDebugLibraries Condition="'$(Configuration)'=='Release'">false</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<CharacterSet>MultiByte</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<PropertyGroup>
|
||||
<OutDir>$(ProjectDir)..\build\$(Platform)\$(Configuration)\</OutDir>
|
||||
<IntDir>$(ProjectDir)..\build\obj\$(ProjectName)\$(Platform)\$(Configuration)\</IntDir>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(EnableAsan)'=='true' And '$(Platform)'=='x64'">
|
||||
<EnableASAN>true</EnableASAN>
|
||||
<OutDir>$(ProjectDir)..\build\$(Platform)\$(Configuration)-asan\</OutDir>
|
||||
<IntDir>$(ProjectDir)..\build\obj\$(ProjectName)\$(Platform)\$(Configuration)-asan\</IntDir>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<WarningLevel>Level4</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
<PreprocessorDefinitions>STELLARX_TESTING;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<AdditionalIncludeDirectories>$(ProjectDir)..\include\StellarX;$(ProjectDir)..\src\StellarX\internal;$(VCInstallDir)Auxiliary\VS\UnitTest\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalDependencies>Microsoft.VisualStudio.TestTools.CppUnitTestFramework.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalLibraryDirectories Condition="'$(Platform)'=='x64'">$(VCInstallDir)Auxiliary\VS\UnitTest\lib\x64;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
<AdditionalLibraryDirectories Condition="'$(Platform)'=='Win32'">$(VCInstallDir)Auxiliary\VS\UnitTest\lib\x86;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup><ClCompile Include="StellarXTests.cpp" /></ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellarX.Core.vcxproj"><Project>{28B8D78E-85AF-4F14-AD56-D51AB6958941}</Project></ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
</Project>
|
||||
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|Win32"><Configuration>Debug</Configuration><Platform>Win32</Platform></ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|Win32"><Configuration>Release</Configuration><Platform>Win32</Platform></ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug|x64"><Configuration>Debug</Configuration><Platform>x64</Platform></ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64"><Configuration>Release</Configuration><Platform>x64</Platform></ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>17.0</VCProjectVersion><Keyword>Win32Proj</Keyword>
|
||||
<ProjectGuid>{D0AF4B0B-7379-4A20-BF10-68CC8DA4D905}</ProjectGuid>
|
||||
<RootNamespace>StellarXUiFixture</RootNamespace><WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries Condition="'$(Configuration)'=='Debug'">true</UseDebugLibraries>
|
||||
<UseDebugLibraries Condition="'$(Configuration)'=='Release'">false</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset><CharacterSet>MultiByte</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<PropertyGroup>
|
||||
<OutDir>$(ProjectDir)..\build\$(Platform)\$(Configuration)\</OutDir>
|
||||
<IntDir>$(ProjectDir)..\build\obj\$(ProjectName)\$(Platform)\$(Configuration)\</IntDir>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile><WarningLevel>Level4</WarningLevel><SDLCheck>true</SDLCheck><ConformanceMode>true</ConformanceMode><LanguageStandard>stdcpp17</LanguageStandard><PrecompiledHeader>NotUsing</PrecompiledHeader><AdditionalIncludeDirectories>$(ProjectDir)..\include\StellarX;$(ProjectDir)..\src\StellarX\internal;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories></ClCompile>
|
||||
<Link><SubSystem>Console</SubSystem><GenerateDebugInformation>true</GenerateDebugInformation></Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup><ClCompile Include="UiFixture.cpp" /></ItemGroup>
|
||||
<ItemGroup><ProjectReference Include="..\StellarX.Core.vcxproj"><Project>{28B8D78E-85AF-4F14-AD56-D51AB6958941}</Project></ProjectReference></ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
</Project>
|
||||
@@ -0,0 +1,373 @@
|
||||
#include "CppUnitTest.h"
|
||||
#include "StellarX.h"
|
||||
#include "SxEventLoopInternal.h"
|
||||
#include "SxTextInternal.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace
|
||||
{
|
||||
// 轻量测试控件只实现生命周期和事件所需的最小行为,避免把所有权测试绑定到真实绘图。
|
||||
class FakeControl final : public Control
|
||||
{
|
||||
public:
|
||||
FakeControl() : Control(0, 0, 10, 10) { id = "Fake"; }
|
||||
void draw() override { dirty = false; }
|
||||
bool handleEvent(const ExMessage&) override { return false; }
|
||||
bool model() const override { return false; }
|
||||
};
|
||||
|
||||
std::filesystem::path MakeTempLogPath(const char* name)
|
||||
{
|
||||
// 使用进程号隔离并行测试产生的日志路径;测试结束后由用例主动清理。
|
||||
return std::filesystem::temp_directory_path() /
|
||||
(std::string("stellarx-") + name + "-" + std::to_string(GetCurrentProcessId()) + ".log");
|
||||
}
|
||||
}
|
||||
|
||||
namespace StellarXNativeTests
|
||||
{
|
||||
// 验证句柄身份和回调期间的延迟变更不会破坏容器遍历或对象生命周期。
|
||||
TEST_CLASS(OwnershipTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(HandlesAreUnique)
|
||||
{
|
||||
// 句柄必须与对象地址解耦,才能在对象被销毁后安全地表示原对象身份。
|
||||
auto first = std::make_unique<FakeControl>();
|
||||
auto second = std::make_unique<FakeControl>();
|
||||
Assert::AreNotEqual(first->getHandle().value, second->getHandle().value);
|
||||
}
|
||||
|
||||
TEST_METHOD(CallbackCanRemoveCurrentControlSafely)
|
||||
{
|
||||
// 回调删除自身后,当前事件栈仍会继续返回,但不能再次访问已释放控件。
|
||||
Window window(200, 120, 1);
|
||||
auto button = std::make_unique<Button>(10, 10, 80, 30, "remove");
|
||||
Button* raw = button.get();
|
||||
const auto handle = window.addControl(std::move(button));
|
||||
raw->setOnClickListener([&]() { Assert::IsTrue(window.removeControl(handle)); });
|
||||
|
||||
ExMessage down{}; down.message = WM_LBUTTONDOWN; down.x = 20; down.y = 20;
|
||||
ExMessage up = down; up.message = WM_LBUTTONUP;
|
||||
raw->handleEvent(down);
|
||||
raw->handleEvent(up);
|
||||
|
||||
Assert::IsNull(window.findControl(handle));
|
||||
Assert::AreEqual<size_t>(0, window.getControlHandles().size());
|
||||
}
|
||||
|
||||
TEST_METHOD(CallbackCanQueueAddAndClear)
|
||||
{
|
||||
// 同一回调内先添加再清空,提交顺序必须确定且不得让遍历迭代器失效。
|
||||
Window window(200, 120, 1);
|
||||
auto button = std::make_unique<Button>(10, 10, 80, 30, "mutate");
|
||||
Button* raw = button.get();
|
||||
window.addControl(std::move(button));
|
||||
raw->setOnClickListener([&]() {
|
||||
window.addControl(std::make_unique<FakeControl>());
|
||||
window.clearControls();
|
||||
});
|
||||
|
||||
ExMessage down{}; down.message = WM_LBUTTONDOWN; down.x = 20; down.y = 20;
|
||||
ExMessage up = down; up.message = WM_LBUTTONUP;
|
||||
raw->handleEvent(down);
|
||||
raw->handleEvent(up);
|
||||
Assert::AreEqual<size_t>(0, window.getControlHandles().size());
|
||||
}
|
||||
|
||||
TEST_METHOD(CallbackCanRemoveSiblingAndLeaveRemainingControlUsable)
|
||||
{
|
||||
// 删除兄弟控件后,未受影响的后续控件仍应能够接收下一次点击。
|
||||
Window window(200, 120, 1);
|
||||
auto remover = std::make_unique<Button>(10, 10, 80, 30, "remove sibling");
|
||||
Button* removerRaw = remover.get();
|
||||
window.addControl(std::move(remover));
|
||||
|
||||
auto sibling = std::make_unique<FakeControl>();
|
||||
const auto siblingHandle = window.addControl(std::move(sibling));
|
||||
int remainingClicks = 0;
|
||||
auto remaining = std::make_unique<Button>(100, 10, 80, 30, "remaining");
|
||||
Button* remainingRaw = remaining.get();
|
||||
remainingRaw->setOnClickListener([&]() { ++remainingClicks; });
|
||||
window.addControl(std::move(remaining));
|
||||
|
||||
removerRaw->setOnClickListener([&]() { window.removeControl(siblingHandle); });
|
||||
ExMessage down{}; down.message = WM_LBUTTONDOWN; down.x = 20; down.y = 20;
|
||||
ExMessage up = down; up.message = WM_LBUTTONUP;
|
||||
removerRaw->handleEvent(down);
|
||||
removerRaw->handleEvent(up);
|
||||
Assert::IsNull(window.findControl(siblingHandle));
|
||||
|
||||
down.x = 120; up.x = 120;
|
||||
remainingRaw->handleEvent(down);
|
||||
remainingRaw->handleEvent(up);
|
||||
Assert::AreEqual(1, remainingClicks);
|
||||
}
|
||||
|
||||
TEST_METHOD(QueuedAddCanBeRemovedBeforeTransactionCommit)
|
||||
{
|
||||
// 队列中的新对象也必须拥有可撤销的句柄,提交前移除不能误伤现有对象。
|
||||
Window window(200, 120, 1);
|
||||
auto canvas = std::make_unique<Canvas>(0, 50, 100, 60);
|
||||
Canvas* canvasRaw = canvas.get();
|
||||
window.addControl(std::move(canvas));
|
||||
|
||||
auto trigger = std::make_unique<Button>(10, 10, 80, 30, "queue");
|
||||
Button* triggerRaw = trigger.get();
|
||||
window.addControl(std::move(trigger));
|
||||
triggerRaw->setOnClickListener([&]() {
|
||||
const auto windowHandle = window.addControl(std::make_unique<FakeControl>());
|
||||
Assert::IsTrue(window.removeControl(windowHandle));
|
||||
const auto canvasHandle = canvasRaw->addControl(std::make_unique<FakeControl>());
|
||||
Assert::IsTrue(canvasRaw->removeControl(canvasHandle));
|
||||
});
|
||||
|
||||
ExMessage down{}; down.message = WM_LBUTTONDOWN; down.x = 20; down.y = 20;
|
||||
ExMessage up = down; up.message = WM_LBUTTONUP;
|
||||
triggerRaw->handleEvent(down);
|
||||
triggerRaw->handleEvent(up);
|
||||
|
||||
Assert::AreEqual<size_t>(2, window.getControlHandles().size());
|
||||
Assert::AreEqual<size_t>(0, canvasRaw->getControlHandles().size());
|
||||
}
|
||||
|
||||
TEST_METHOD(TabAddedFromCallbackCommitsWithPendingPageControl)
|
||||
{
|
||||
// 页签及其页面控件在同一事务中加入时,页面内部的排队变更也必须一并提交。
|
||||
Window window(300, 200, 1);
|
||||
auto tabs = std::make_unique<TabControl>(0, 50, 200, 140);
|
||||
TabControl* tabsRaw = tabs.get();
|
||||
tabsRaw->addTab(std::make_unique<Button>(0, 0, 80, 24, "first"),
|
||||
std::make_unique<Canvas>(0, 0, 200, 110));
|
||||
window.addControl(std::move(tabs));
|
||||
|
||||
auto trigger = std::make_unique<Button>(10, 10, 80, 30, "add tab");
|
||||
Button* triggerRaw = trigger.get();
|
||||
window.addControl(std::move(trigger));
|
||||
triggerRaw->setOnClickListener([&]() {
|
||||
const size_t index = tabsRaw->addTab(
|
||||
std::make_unique<Button>(0, 0, 80, 24, "second"),
|
||||
std::make_unique<Canvas>(0, 0, 200, 110));
|
||||
auto rejected = tabsRaw->tryAddToPage(index, std::make_unique<FakeControl>());
|
||||
Assert::IsNull(rejected.get());
|
||||
});
|
||||
|
||||
ExMessage down{}; down.message = WM_LBUTTONDOWN; down.x = 20; down.y = 20;
|
||||
ExMessage up = down; up.message = WM_LBUTTONUP;
|
||||
triggerRaw->handleEvent(down);
|
||||
triggerRaw->handleEvent(up);
|
||||
Assert::AreEqual(2, tabsRaw->count());
|
||||
}
|
||||
};
|
||||
|
||||
// 验证 MBCS 文本所有权、空页签边界、提示延时和密码掩码等纯逻辑行为。
|
||||
TEST_CLASS(TextAndTabTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(ButtonConstructsWithChineseTextStyle)
|
||||
{
|
||||
// Button 的字体样式含有 std::string;构造中文按钮可回归成员初始化顺序错误。
|
||||
Button button(10, 10, 80, 30, "圆");
|
||||
Assert::AreEqual(std::string("圆"), button.getButtonText());
|
||||
Assert::AreEqual(std::string("微软雅黑"), button.getButtonTextStyle().faceName);
|
||||
}
|
||||
|
||||
TEST_METHOD(ControlTextOwnsAndComparesFaceName)
|
||||
{
|
||||
// 字体名应复制字符串内容,而不是保存临时缓冲区地址或比较地址。
|
||||
StellarX::ControlText first;
|
||||
first.faceName = std::string("Temporary Font");
|
||||
StellarX::ControlText second = first;
|
||||
Assert::IsTrue(first == second);
|
||||
second.faceName = "Other Font";
|
||||
Assert::IsTrue(first != second);
|
||||
Assert::AreEqual(std::string("Temporary Font"), first.faceName);
|
||||
}
|
||||
|
||||
TEST_METHOD(TabRejectsInvalidPageWithoutConsumingControl)
|
||||
{
|
||||
// 非法页索引必须原样返还控件所有权,空页签也不能越界访问。
|
||||
TabControl tabs(0, 0, 300, 200);
|
||||
Assert::IsTrue(tabs.setActiveIndex(0));
|
||||
auto rejected = tabs.tryAddToPage(0, std::make_unique<FakeControl>());
|
||||
Assert::IsNotNull(rejected.get());
|
||||
|
||||
const size_t page = tabs.addTab(
|
||||
std::make_unique<Button>(0, 0, 80, 24, "same"),
|
||||
std::make_unique<Canvas>(0, 0, 300, 176));
|
||||
Assert::AreEqual<size_t>(0, page);
|
||||
auto accepted = tabs.tryAddToPage(page, std::move(rejected));
|
||||
Assert::IsNull(accepted.get());
|
||||
}
|
||||
|
||||
TEST_METHOD(TooltipAppearsOnTickWithoutAdditionalMouseMove)
|
||||
{
|
||||
// 鼠标停留后的显示由时钟驱动,不应要求额外的 WM_MOUSEMOVE 才能到期。
|
||||
FakeControl repaintSink;
|
||||
Button button(10, 10, 80, 30, "tooltip");
|
||||
button.setParent(&repaintSink);
|
||||
button.enableTooltip(true);
|
||||
button.setTooltipDelay(1000);
|
||||
ExMessage move{}; move.message = WM_MOUSEMOVE; move.x = 20; move.y = 20;
|
||||
Assert::IsTrue(button.handleEvent(move));
|
||||
Assert::IsFalse(button.isTooltipVisible());
|
||||
|
||||
const ULONGLONG afterMove = GetTickCount64();
|
||||
button.tick(afterMove);
|
||||
Assert::IsFalse(button.isTooltipVisible());
|
||||
button.tick(afterMove + 1000);
|
||||
Assert::IsTrue(button.isTooltipVisible());
|
||||
move.x = 200; move.y = 100;
|
||||
Assert::IsFalse(button.handleEvent(move));
|
||||
Assert::IsFalse(button.isTooltipVisible());
|
||||
|
||||
move.x = 20; move.y = 20;
|
||||
button.handleEvent(move);
|
||||
button.tick(GetTickCount64() + 1000);
|
||||
Assert::IsTrue(button.isTooltipVisible());
|
||||
button.enableTooltip(false);
|
||||
Assert::IsFalse(button.isTooltipVisible());
|
||||
}
|
||||
|
||||
TEST_METHOD(PasswordMaskCountsMbcCharactersWithoutOverrun)
|
||||
{
|
||||
// 掩码按 MBCS 逻辑字符计数,同时覆盖不完整和非法字节序列。
|
||||
Assert::AreEqual(std::string("***"), StellarX::Internal::makePasswordMask("abc"));
|
||||
const std::string gbk = std::string("\xD6\xD0\xCE\xC4", 4) + "A";
|
||||
Assert::AreEqual(std::string("***"), StellarX::Internal::makePasswordMask(gbk));
|
||||
const std::string incomplete = std::string("A\x81", 2);
|
||||
Assert::AreEqual(std::string("**"), StellarX::Internal::makePasswordMask(incomplete));
|
||||
const std::string invalidPair = std::string("\x81\x30", 2);
|
||||
Assert::AreEqual(std::string("**"), StellarX::Internal::makePasswordMask(invalidPair));
|
||||
}
|
||||
};
|
||||
|
||||
// 验证消息批处理上限、空闲休眠策略和关闭消息的停止语义。
|
||||
TEST_CLASS(EventLoopTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(ThousandMessagesDrainInEightActiveRounds)
|
||||
{
|
||||
// 每轮最多处理固定数量消息,活动轮不休眠,1000 条消息应在 8 轮内排空。
|
||||
std::vector<ExMessage> messages(1000);
|
||||
size_t next = 0;
|
||||
size_t handled = 0;
|
||||
int rounds = 0;
|
||||
int activeSleeps = 0;
|
||||
ExMessage current{};
|
||||
while (next < messages.size())
|
||||
{
|
||||
const auto batch = StellarX::Internal::drainMessageBatch(current,
|
||||
[&](ExMessage& out) {
|
||||
if (next >= messages.size()) return false;
|
||||
out = messages[next++];
|
||||
return true;
|
||||
},
|
||||
[&](const ExMessage&) { ++handled; return true; });
|
||||
++rounds;
|
||||
if (batch.shouldSleep()) ++activeSleeps;
|
||||
}
|
||||
|
||||
Assert::AreEqual<size_t>(1000, handled);
|
||||
Assert::AreEqual(8, rounds);
|
||||
Assert::AreEqual(0, activeSleeps);
|
||||
const auto idle = StellarX::Internal::drainMessageBatch(current,
|
||||
[](ExMessage&) { return false; }, [](const ExMessage&) { return true; });
|
||||
Assert::IsTrue(idle.shouldSleep());
|
||||
}
|
||||
|
||||
TEST_METHOD(CloseStopsBatchAfterEarlierResize)
|
||||
{
|
||||
// 批次内遇到 WM_CLOSE 后停止继续取消息,之前的 WM_SIZE 仍必须保留。
|
||||
std::vector<ExMessage> messages(3);
|
||||
messages[0].message = WM_SIZE;
|
||||
messages[1].message = WM_CLOSE;
|
||||
messages[2].message = WM_MOUSEMOVE;
|
||||
size_t next = 0;
|
||||
std::vector<UINT> handled;
|
||||
ExMessage current{};
|
||||
const auto batch = StellarX::Internal::drainMessageBatch(current,
|
||||
[&](ExMessage& out) {
|
||||
if (next >= messages.size()) return false;
|
||||
out = messages[next++];
|
||||
return true;
|
||||
},
|
||||
[&](const ExMessage& message) {
|
||||
handled.push_back(message.message);
|
||||
return message.message != WM_CLOSE;
|
||||
});
|
||||
|
||||
Assert::IsTrue(batch.stopped);
|
||||
Assert::AreEqual<size_t>(2, batch.processed);
|
||||
Assert::AreEqual<size_t>(2, handled.size());
|
||||
Assert::AreEqual<UINT>(WM_SIZE, handled[0]);
|
||||
Assert::AreEqual<UINT>(WM_CLOSE, handled[1]);
|
||||
Assert::AreEqual<size_t>(2, next);
|
||||
}
|
||||
};
|
||||
|
||||
// 验证文件 sink 的原子切换、失败回退和轮换过程不会丢失日志行。
|
||||
TEST_CLASS(LoggingTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(SetConfigOpensAndDisablesFileAtomically)
|
||||
{
|
||||
// 无效的新文件配置不能覆盖当前可用配置,也不能关闭旧 sink。
|
||||
const auto path = MakeTempLogPath("config");
|
||||
std::filesystem::remove(path);
|
||||
auto& logger = StellarX::SxLogger::Get();
|
||||
StellarX::SxLogConfig config;
|
||||
config.fileEnabled = true;
|
||||
config.filePath = path.string();
|
||||
config.fileAppend = false;
|
||||
Assert::IsTrue(logger.setConfig(config));
|
||||
logger.logLine(StellarX::SxLogLevel::Info, "test", __FILE__, __LINE__, __func__, "configured");
|
||||
logger.disableFile();
|
||||
Assert::IsTrue(std::filesystem::exists(path));
|
||||
|
||||
const auto before = logger.getConfigCopy();
|
||||
config.fileEnabled = true;
|
||||
config.filePath.clear();
|
||||
Assert::IsFalse(logger.setConfig(config));
|
||||
Assert::AreEqual(before.filePath, logger.getConfigCopy().filePath);
|
||||
std::filesystem::remove(path);
|
||||
}
|
||||
|
||||
TEST_METHOD(RotationPreservesEveryLine)
|
||||
{
|
||||
// 高频轮换可能发生在同一毫秒内,所有分片和当前文件合计必须保留每一行。
|
||||
const auto path = MakeTempLogPath("rotate");
|
||||
const auto directory = path.parent_path();
|
||||
const auto prefix = path.filename().string() + ".";
|
||||
std::filesystem::remove(path);
|
||||
for (const auto& entry : std::filesystem::directory_iterator(directory))
|
||||
if (entry.path().filename().string().rfind(prefix, 0) == 0) std::filesystem::remove(entry.path());
|
||||
|
||||
StellarX::FileSink sink;
|
||||
sink.setRotateBytes(32);
|
||||
Assert::IsTrue(sink.open(path.string(), false));
|
||||
for (int i = 0; i < 30; ++i) sink.writeLine("line-" + std::to_string(i) + "\n");
|
||||
sink.close();
|
||||
|
||||
int lineCount = 0;
|
||||
for (const auto& entry : std::filesystem::directory_iterator(directory))
|
||||
{
|
||||
const auto name = entry.path().filename().string();
|
||||
if (entry.path() != path && name.rfind(prefix, 0) != 0) continue;
|
||||
{
|
||||
std::ifstream input(entry.path());
|
||||
std::string line;
|
||||
while (std::getline(input, line)) ++lineCount;
|
||||
}
|
||||
std::filesystem::remove(entry.path());
|
||||
}
|
||||
Assert::AreEqual(30, lineCount);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
#include "StellarX.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace
|
||||
{
|
||||
// 场景使用固定纯色,脚本只断言关键像素,不依赖字体抗锯齿或完整截图。
|
||||
constexpr COLORREF Background = RGB(30, 50, 70);
|
||||
constexpr COLORREF ButtonColor = RGB(80, 160, 220);
|
||||
constexpr COLORREF ClickColor = RGB(220, 90, 70);
|
||||
constexpr COLORREF HoverColor = RGB(250, 200, 80);
|
||||
|
||||
void WriteResult(const std::filesystem::path& resultDir, const std::string& scenario,
|
||||
const std::string& payload)
|
||||
{
|
||||
// JSON 是脚本与 fixture 之间的稳定协议;先建目录,再一次性覆盖当前检查点。
|
||||
std::filesystem::create_directories(resultDir);
|
||||
std::ofstream output(resultDir / (scenario + ".json"), std::ios::trunc);
|
||||
output << payload;
|
||||
}
|
||||
|
||||
std::unique_ptr<Button> MakeButton(int x, int y, int w, int h, const std::string& text)
|
||||
{
|
||||
// 统一按钮颜色,使 UI 回归可以区分背景、悬停和点击后的绘制结果。
|
||||
return std::make_unique<Button>(x, y, w, h, text, ClickColor, ButtonColor, HoverColor);
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
// fixture 每次只运行一个确定性场景,并用唯一标题让驱动脚本精确绑定 HWND。
|
||||
const std::string scenario = argc > 1 ? argv[1] : "geometry";
|
||||
const std::filesystem::path resultDir = argc > 2 ? argv[2] : "TestResults";
|
||||
Window window(480, 320, 1, Background, "StellarX.UI." + scenario);
|
||||
|
||||
if (scenario == "ownership")
|
||||
{
|
||||
// 点击后删除自身并新增状态标签,覆盖回调期间删除和新增的生命周期路径。
|
||||
auto button = MakeButton(20, 20, 120, 40, "remove self");
|
||||
Button* raw = button.get();
|
||||
const auto handle = window.addControl(std::move(button));
|
||||
raw->setOnClickListener([&]() {
|
||||
window.removeControl(handle);
|
||||
auto status = std::make_unique<Label>(20, 90, "removed safely", WHITE, Background);
|
||||
status->setTextdisap(true);
|
||||
window.addControl(std::move(status));
|
||||
WriteResult(resultDir, scenario, "{\"removed\":true}");
|
||||
});
|
||||
}
|
||||
else if (scenario == "geometry")
|
||||
{
|
||||
// 一次点击同时移动、缩放和隐藏多个控件,用于检查旧区域是否被完整恢复。
|
||||
auto button = MakeButton(20, 20, 120, 40, "move");
|
||||
Button* raw = button.get();
|
||||
auto label = std::make_unique<Label>(20, 85, "hide label", WHITE, RGB(90, 130, 70));
|
||||
Label* labelRaw = label.get();
|
||||
auto textBox = std::make_unique<TextBox>(20, 120, 120, 36, "resize");
|
||||
TextBox* textBoxRaw = textBox.get();
|
||||
textBoxRaw->setTextBoxBk(RGB(230, 230, 210));
|
||||
auto canvas = std::make_unique<Canvas>(20, 175, 120, 50);
|
||||
Canvas* canvasRaw = canvas.get();
|
||||
canvasRaw->setCanvasBkColor(RGB(40, 120, 90));
|
||||
|
||||
raw->setOnClickListener([&, raw, labelRaw, textBoxRaw, canvasRaw]() {
|
||||
raw->setX(220);
|
||||
labelRaw->setIsVisible(false);
|
||||
textBoxRaw->setX(220);
|
||||
textBoxRaw->setWidth(80);
|
||||
canvasRaw->setX(220);
|
||||
canvasRaw->setWidth(80);
|
||||
WriteResult(resultDir, scenario,
|
||||
"{\"moved\":true,\"x\":220,\"textWidth\":80,\"canvasWidth\":80,\"labelVisible\":false}");
|
||||
});
|
||||
window.addControl(std::move(button));
|
||||
window.addControl(std::move(label));
|
||||
window.addControl(std::move(textBox));
|
||||
window.addControl(std::move(canvas));
|
||||
}
|
||||
else if (scenario == "tooltip")
|
||||
{
|
||||
// 场景不再发送第二次鼠标移动,专门验证 Window tick 能让静止提示到期显示。
|
||||
auto button = MakeButton(20, 20, 120, 40, "tooltip");
|
||||
button->enableTooltip(true);
|
||||
button->setTooltipDelay(200);
|
||||
button->setTooltipText("stationary tip");
|
||||
window.addControl(std::move(button));
|
||||
}
|
||||
else if (scenario == "tab")
|
||||
{
|
||||
// 两个页签故意使用相同标题,验证索引身份而不是文本匹配决定激活状态。
|
||||
auto tabs = std::make_unique<TabControl>(20, 20, 300, 180);
|
||||
TabControl* raw = tabs.get();
|
||||
raw->setActiveIndex(0);
|
||||
auto firstButton = MakeButton(0, 0, 100, 30, "same");
|
||||
Button* firstButtonRaw = firstButton.get();
|
||||
auto secondButton = MakeButton(0, 0, 100, 30, "same");
|
||||
Button* secondButtonRaw = secondButton.get();
|
||||
auto firstPage = std::make_unique<Canvas>(0, 0, 300, 150);
|
||||
Canvas* firstPageRaw = firstPage.get();
|
||||
firstPageRaw->setCanvasBkColor(RGB(40, 100, 80));
|
||||
auto secondPage = std::make_unique<Canvas>(0, 0, 300, 150);
|
||||
Canvas* secondPageRaw = secondPage.get();
|
||||
secondPageRaw->setCanvasBkColor(RGB(110, 70, 120));
|
||||
raw->addTab(std::move(firstButton), std::move(firstPage));
|
||||
raw->addTab(std::move(secondButton), std::move(secondPage));
|
||||
window.addControl(std::move(tabs));
|
||||
auto verify = MakeButton(340, 220, 110, 36, "verify");
|
||||
verify->setOnClickListener([&, raw, firstButtonRaw, secondButtonRaw, firstPageRaw, secondPageRaw]() {
|
||||
WriteResult(resultDir, scenario, std::string("{\"active\":") +
|
||||
std::to_string(raw->getActiveIndex()) +
|
||||
",\"firstSelected\":" + (firstButtonRaw->isClicked() ? "true" : "false") +
|
||||
",\"secondSelected\":" + (secondButtonRaw->isClicked() ? "true" : "false") +
|
||||
",\"firstPageVisible\":" + (firstPageRaw->IsVisible() ? "true" : "false") +
|
||||
",\"secondPageVisible\":" + (secondPageRaw->IsVisible() ? "true" : "false") + "}");
|
||||
});
|
||||
window.addControl(std::move(verify));
|
||||
}
|
||||
else if (scenario == "table")
|
||||
{
|
||||
// 长表头和长单元格配合极窄宽度,验证最小宽度夹紧及 coverage 报告范围。
|
||||
auto table = std::make_unique<Table>(20, 20);
|
||||
Table* raw = table.get();
|
||||
raw->setHeaders({ "very-long-header", "second-long-header", "third" });
|
||||
raw->setData({ { "very-long-cell-value", "another-long-value", "value" } });
|
||||
window.addControl(std::move(table));
|
||||
auto resize = MakeButton(340, 250, 110, 36, "narrow");
|
||||
resize->setOnClickListener([&, raw]() {
|
||||
raw->setWidth(40);
|
||||
const RECT coverage = raw->getManagedRepaintCoverageRect();
|
||||
WriteResult(resultDir, scenario, std::string("{\"width\":") +
|
||||
std::to_string(raw->getWidth()) +
|
||||
",\"coverageLeft\":" + std::to_string(coverage.left) +
|
||||
",\"coverageTop\":" + std::to_string(coverage.top) +
|
||||
",\"coverageRight\":" + std::to_string(coverage.right) +
|
||||
",\"coverageBottom\":" + std::to_string(coverage.bottom) + "}");
|
||||
});
|
||||
window.addControl(std::move(resize));
|
||||
}
|
||||
else if (scenario == "dialog")
|
||||
{
|
||||
// 先叠加两个非模态对话框,再只关闭上层,验证下层恢复和交互顺序。
|
||||
struct DialogState { bool upperClosed = false; bool lowerClosed = false; };
|
||||
auto state = std::make_shared<DialogState>();
|
||||
auto writeDialogState = [&, state]() {
|
||||
WriteResult(resultDir, scenario, std::string("{\"upperClosed\":") +
|
||||
(state->upperClosed ? "true" : "false") + ",\"lowerClosed\":" +
|
||||
(state->lowerClosed ? "true" : "false") + "}");
|
||||
};
|
||||
auto open = MakeButton(20, 20, 140, 40, "open dialogs");
|
||||
open->setOnClickListener([&, state, writeDialogState]() {
|
||||
StellarX::MessageBox::showAsync(window, "lower", "lower", StellarX::MessageBoxType::OK,
|
||||
[state, writeDialogState](StellarX::MessageBoxResult) { state->lowerClosed = true; writeDialogState(); });
|
||||
StellarX::MessageBox::showAsync(window, "upper", "upper", StellarX::MessageBoxType::OK,
|
||||
[state, writeDialogState](StellarX::MessageBoxResult) { state->upperClosed = true; writeDialogState(); });
|
||||
});
|
||||
window.addControl(std::move(open));
|
||||
}
|
||||
else
|
||||
{
|
||||
// 未知场景按参数错误处理,避免脚本误把未覆盖场景当成成功。
|
||||
WriteResult(resultDir, scenario, "{\"error\":\"unknown scenario\"}");
|
||||
return 2;
|
||||
}
|
||||
|
||||
window.draw();
|
||||
return window.runEventLoop();
|
||||
}
|
||||
@@ -48,6 +48,13 @@
|
||||
- 日志降噪
|
||||
- 模板体系与开发记录目录建立
|
||||
|
||||
7. 第五阶段:全量缺陷修复与自动化验证(当前工作区)
|
||||
- 主要内容:
|
||||
- 新增 `StellarX.Core` 核心静态库,拆分框架实现与应用入口
|
||||
- 修复控件所有权、空页签、快照、几何残影、Dialog、Table、日志、MBCS、Tooltip 和事件循环问题
|
||||
- 新增 Native Unit Test、x64 ASan、EasyX UI Fixture 和 PowerShell 自动化测试入口
|
||||
- 完成 Debug x64 / ASan、Release x64、Debug Win32 逻辑测试,以及 UI 三轮回归
|
||||
|
||||
## 记录索引
|
||||
|
||||
### BUG
|
||||
@@ -67,10 +74,17 @@
|
||||
- [Feature-20260409-0003 输入事件、hover 与遮挡交互调整](./功能变更/Feature-20260409-0003-输入事件与遮挡交互调整.md)
|
||||
- [Feature-20260409-0004 测试用例与可观测性调整](./功能变更/Feature-20260409-0004-测试用例与可观测性调整.md)
|
||||
- [Feature-20260409-0005 开发记录与模板体系整理](./功能变更/Feature-20260409-0005-开发记录与模板体系整理.md)
|
||||
- [Feature-20260915-0001 全量缺陷修复与 API 调整](./功能变更/Feature-20260915-0001-全量缺陷修复与API调整.md)
|
||||
|
||||
### 模块
|
||||
|
||||
- [Module-20260409-0001 Window 托管重绘与覆盖合成机制](./模块/Module-20260409-0001-Window托管重绘与覆盖合成机制.md)
|
||||
- [Module-20260915-0001 StellarX 核心库与自动化测试基础设施](./模块/Module-20260915-0001-StellarX核心库与自动化测试基础设施.md)
|
||||
|
||||
### 配套文档
|
||||
|
||||
- [MIGRATION.md](./MIGRATION.md):破坏性 API 迁移说明
|
||||
- [TESTING.md](./TESTING.md):测试入口、覆盖范围和诊断产物说明
|
||||
|
||||
## 覆盖关系
|
||||
|
||||
@@ -135,3 +149,21 @@
|
||||
- 第二阶段托管重绘
|
||||
- 注释补充与校正
|
||||
- 记录与模板体系整理
|
||||
|
||||
## 第五阶段覆盖关系
|
||||
|
||||
- `StellarX.Core.vcxproj`
|
||||
- 统一承载框架源码和公共/内部头文件。
|
||||
- 关联:
|
||||
- [Module-20260915-0001 StellarX 核心库与自动化测试基础设施](./模块/Module-20260915-0001-StellarX核心库与自动化测试基础设施.md)
|
||||
- [Feature-20260915-0001 全量缺陷修复与 API 调整](./功能变更/Feature-20260915-0001-全量缺陷修复与API调整.md)
|
||||
|
||||
- `tests / scripts`
|
||||
- 覆盖逻辑单元测试、ASan、EasyX UI Fixture、关键像素断言和诊断产物。
|
||||
- 关联:
|
||||
- [TESTING.md](./TESTING.md)
|
||||
- [Module-20260915-0001 StellarX 核心库与自动化测试基础设施](./模块/Module-20260915-0001-StellarX核心库与自动化测试基础设施.md)
|
||||
|
||||
- `Control / Window / Canvas / Button / Label / TextBox / Table / Dialog / TabControl / SxLogger`
|
||||
- 本批次高、中、低严重度问题和验证用例统一记录于:
|
||||
- [Feature-20260915-0001 全量缺陷修复与 API 调整](./功能变更/Feature-20260915-0001-全量缺陷修复与API调整.md)
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
## 根因
|
||||
|
||||
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp) 的 `runEventLoop()` resize 收口中存在一段历史“跨度保护”。
|
||||
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Window.cpp) 的 `runEventLoop()` resize 收口中存在一段历史“跨度保护”。
|
||||
- 该保护把“宽高变化超过 1000”视为异常帧,直接终止收口:
|
||||
- 不再执行 `adaptiveLayout`
|
||||
- 不再执行 `Resize`
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# BUG-20251209-0001
|
||||
|
||||
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: BUG-20251209-0001
|
||||
- 标题: TabControl 由不可见设置为可见时绘制错乱
|
||||
- 状态:已修复
|
||||
- 严重性:S3
|
||||
- 优先级:P2
|
||||
- 模块: TabControl
|
||||
- 版本 / 分支: V2.3.1
|
||||
- 环境: Visual Studio / Debug / Release
|
||||
- 发现人: 用户
|
||||
- 关联 Fix ID: Fix-BUG-20251209-0001
|
||||
|
||||
## 问题描述
|
||||
|
||||
- 现象: 调用 `TabControl::setIsVisible()` 将选项卡由不可见变为可见时,会绘制出未激活的页面。
|
||||
- 影响范围: 多页面 TabControl 在隐藏后重新显示时可能出现页面叠加,造成绘制错乱并影响用户体验。
|
||||
- 期望结果: TabControl 可见后只绘制当前激活页;如果没有激活页,则不绘制任何页。
|
||||
- 实际结果: 所有页面被绘制出来,导致未激活页和激活页重叠。
|
||||
|
||||
## 复现信息
|
||||
|
||||
- 前置条件:[可选] 存在至少两个页面的 TabControl。
|
||||
- 复现步骤:
|
||||
|
||||
1. 将 TabControl 设置为不可见。
|
||||
2. 通过事件或回调将 TabControl 设置为可见。
|
||||
3. 观察页面绘制结果。
|
||||
|
||||
- 复现概率:必现
|
||||
- 最小复现 Demo:[可选] TabControl 隐藏 / 显示切换场景
|
||||
- 证据:PDF 历史记录描述隐藏后恢复显示会绘制所有页;初步定位到 `TabControl::setIsVisible()` 的可见性传播逻辑。
|
||||
|
||||
## 初步分析
|
||||
|
||||
- 疑似位置: `TabControl::setIsVisible()`
|
||||
- 触发条件: TabControl 从不可见切换为可见时,对子页面统一传播可见状态。
|
||||
- 相关线索:
|
||||
- `show` 状态恢复为 true 后,非激活页也参与绘制。
|
||||
- TabControl 的页面可见性没有与当前激活状态保持一致。
|
||||
- 最近相关改动:[可选]
|
||||
|
||||
## 跟踪信息
|
||||
|
||||
- 首次发现时间: 2025-12-09
|
||||
- 最后更新时间: 2026-09-19(从 PDF 整理入库)
|
||||
- 修复版本: v2.3.2
|
||||
- 验证版本:[可选] v2.3.2
|
||||
- 备注:[可选] 来源原始 ID 为 `BUG-20251209`;PDF 未包含 `-XXXX` 后缀,入库时补充序号。
|
||||
@@ -0,0 +1,60 @@
|
||||
# BUG-20251220-0001
|
||||
|
||||
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: BUG-20251220-0001
|
||||
- 标题: TabControl::setActiveIndex 绘制前调用崩溃
|
||||
- 状态:已修复
|
||||
- 严重性:S1
|
||||
- 优先级:P0
|
||||
- 模块: TabControl
|
||||
- 版本 / 分支: v2.3.1
|
||||
- 环境: Visual Studio / Debug / Release
|
||||
- 发现人: 用户
|
||||
- 关联 Fix ID: Fix-BUG-20251220-0001
|
||||
|
||||
## 问题描述
|
||||
|
||||
- 现象: 在 TabControl 首次绘制前或进入主事件循环前调用 `setActiveIndex()`,会触发空指针访问并导致程序崩溃。
|
||||
- 影响范围: 影响默认激活页设置流程,启动阶段即可崩溃,严重影响使用。
|
||||
- 期望结果: `setActiveIndex()` 可在绘制前记录默认激活页,并在 TabControl 第一次绘制时完成默认激活。
|
||||
- 实际结果: 绘制前调用会直接崩溃,无法设置默认激活页。
|
||||
|
||||
## 复现信息
|
||||
|
||||
- 前置条件:[可选] TabControl 尚未绘制。
|
||||
- 复现步骤:
|
||||
|
||||
1. 创建 TabControl 对象。
|
||||
2. 在首次绘制前调用 `setActiveIndex(0)`。
|
||||
3. 运行程序并观察崩溃。
|
||||
|
||||
- 复现概率:必现
|
||||
- 最小复现 Demo:
|
||||
|
||||
```cpp
|
||||
auto ta = std::make_unique<TabControl>(10, 10, 1100, 300);
|
||||
ta->setShape(StellarX::ControlShape::B_ROUND_RECTANGLE);
|
||||
ta->setActiveIndex(0);
|
||||
```
|
||||
|
||||
- 证据:PDF 历史记录描述绘制前调用会造成空指针访问。
|
||||
|
||||
## 初步分析
|
||||
|
||||
- 疑似位置: `TabControl::setActiveIndex()`
|
||||
- 触发条件: TabControl / 页签按钮尚未完成绘制和初始化时,提前执行激活页切换逻辑。
|
||||
- 相关线索:
|
||||
- 默认激活应属于延迟到首次绘制后的行为。
|
||||
- 绘制前直接触发页签按钮点击回调,会访问尚未初始化的内部状态。
|
||||
- 最近相关改动:[可选]
|
||||
|
||||
## 跟踪信息
|
||||
|
||||
- 首次发现时间: 2025-12-20
|
||||
- 最后更新时间: 2026-09-19(从 PDF 整理入库)
|
||||
- 修复版本: v2.3.2
|
||||
- 验证版本:[可选] v2.3.2
|
||||
- 备注:[可选] 来源原始 ID 为 `BUG-20251220`;PDF 未包含 `-XXXX` 后缀,入库时补充序号。
|
||||
@@ -0,0 +1,66 @@
|
||||
# BUG-20260109-0001
|
||||
|
||||
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: BUG-20260109-0001
|
||||
- 标题: TextBox::setText 进入事件循环前调用崩溃
|
||||
- 状态:已修复
|
||||
- 严重性:S1
|
||||
- 优先级:P0
|
||||
- 模块: TextBox
|
||||
- 版本 / 分支: v2.3.2(基础版 EasyX + Win32)
|
||||
- 环境: Windows 10/11 x64 + Visual Studio 2022(MSVC)Debug / Release
|
||||
- 发现人: 用户
|
||||
- 关联 Fix ID: Fix-BUG-20260109-0001
|
||||
|
||||
## 问题描述
|
||||
|
||||
- 现象: 在 Window 尚未创建或图形上下文尚未初始化时调用 `TextBox::setText()` 设置默认文本,会触发 `TextBox::draw()`,进而进入依赖 EasyX 图形上下文的绘制 / 抓屏逻辑,导致访问冲突或中断。
|
||||
- 影响范围: 所有“先创建控件并设置默认文本,再创建 Window / 挂载控件树”的初始化流程。
|
||||
- 期望结果: `setText()` 只更新控件状态并标记需要重绘,不应在生命周期未就绪时主动绘制。
|
||||
- 实际结果: `setText()` 内部直接调用 `draw()`,绘制链路在图形上下文未就绪时崩溃。
|
||||
|
||||
## 复现信息
|
||||
|
||||
- 前置条件:[可选] Window / EasyX 图形上下文尚未初始化。
|
||||
- 复现步骤:
|
||||
|
||||
1. 在创建 Window 或首次 `draw()` 前创建 TextBox。
|
||||
2. 调用 `TextBox::setText()` 设置默认文本。
|
||||
3. 随后才创建 Window 或将 TextBox 添加到 Canvas / Window。
|
||||
4. 观察程序在 `setText()` 阶段崩溃。
|
||||
|
||||
- 复现概率:必现
|
||||
- 最小复现 Demo:
|
||||
|
||||
```cpp
|
||||
Window window(1024, 768, NULL, RGB(255, 255, 255), "StellarX Application");
|
||||
|
||||
auto initLibPathBox = std::make_unique<TextBox>(230, 230, 700, 30);
|
||||
initLibPathBox->setText("textText");
|
||||
|
||||
window.addControl(std::move(initLibPathBox));
|
||||
window.draw();
|
||||
return window.runEventLoop();
|
||||
```
|
||||
|
||||
- 证据:PDF 历史记录描述 `setText()` 触发 `draw()` 后进入 `saveBackground()` / `getimage()` 等图形上下文相关逻辑。
|
||||
|
||||
## 初步分析
|
||||
|
||||
- 疑似位置: `TextBox::setText()`
|
||||
- 触发条件: 控件属性设置发生在 Window 绘制 / EasyX 图形上下文初始化之前。
|
||||
- 相关线索:
|
||||
- setter 不应带有依赖图形上下文的即时绘制副作用。
|
||||
- `draw()` 会进入背景快照与图像抓取链路。
|
||||
- 最近相关改动:[可选]
|
||||
|
||||
## 跟踪信息
|
||||
|
||||
- 首次发现时间: 2026-01-09
|
||||
- 最后更新时间: 2026-09-19(从 PDF 整理入库)
|
||||
- 修复版本: v3.0.0
|
||||
- 验证版本:[可选] v3.0.0
|
||||
- 备注:[可选] 来源原始 ID 为 `BUG-20260109`;同日另有 Table 重影问题,入库时按顺序拆为 `0001` 与 `0002`。
|
||||
@@ -0,0 +1,60 @@
|
||||
# BUG-20260109-0002
|
||||
|
||||
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: BUG-20260109-0002
|
||||
- 标题: TabControl 切换后 Table 新旧内容重叠
|
||||
- 状态:已修复
|
||||
- 严重性:S2
|
||||
- 优先级:P1
|
||||
- 模块: TabControl / Canvas / Table / 背景快照 / 脏标记重绘链路
|
||||
- 版本 / 分支: 基础版(EasyX + Win32)/ v2.3.2
|
||||
- 环境: Windows x64 / Visual Studio 2022 / EasyX(Debug / Release 均可能)
|
||||
- 发现人: 用户
|
||||
- 关联 Fix ID: Fix-BUG-20260109-0002
|
||||
|
||||
## 问题描述
|
||||
|
||||
- 现象: 运行 TabControl 示例后,切换到“按钮1”页签并触发“重置表格数据”,随后打开 / 关闭其他页签再切回“按钮1”,Table 稳定出现旧内容与新内容叠加;偶发情况下重置数据当次即出现重影。
|
||||
- 影响范围: 使用背景快照擦除与局部重绘组合的容器 / 控件,尤其是 Table 透明文字背景与 Null 填充模式场景。
|
||||
- 期望结果: 切换页签、关闭页签、重置数据后,Table 只显示当前数据,不残留旧像素。
|
||||
- 实际结果: Table 区域出现旧行文本像素残留,与新内容叠在同一位置。
|
||||
|
||||
## 复现信息
|
||||
|
||||
- 前置条件:[可选] TabControl 示例中包含 Table 和“重置表格数据”按钮。
|
||||
- 复现步骤:
|
||||
|
||||
1. 运行测试程序(TabControl 示例)。
|
||||
2. 切换到“按钮1”页签。
|
||||
3. 点击“重置表格数据”(清空并重新 `setData()`)。
|
||||
4. 随意切换到其他页签,可伴随关闭页签操作。
|
||||
5. 再切回“按钮1”。
|
||||
6. 观察 Table 区域是否出现旧内容与新内容重叠。
|
||||
|
||||
- 复现概率:必现;重置数据当次直接重影为偶现。
|
||||
- 最小复现 Demo:`z-testDome.cpp` 中 TabControl + Table + 重置按钮场景;Null FillMode 与透明文字背景更明显。
|
||||
- 证据:日志显示 Canvas 快照刚丢弃或未就绪时,Table 可见性恢复立即触发局部重绘,随后 `saveBackground()` 把已绘制像素截入背景快照。
|
||||
|
||||
## 初步分析
|
||||
|
||||
- 疑似位置:
|
||||
- `Control::setIsVisible(true)`
|
||||
- `Canvas::requestRepaint()`
|
||||
- `TabControl::requestRepaint()`
|
||||
- 触发条件: 页签切换 / 关闭期间,父容器快照未就绪或 dirty 未清理时,子控件抢跑执行局部绘制。
|
||||
- 相关线索:
|
||||
- `show=true` 时立即向上 `requestRepaint()`。
|
||||
- `Canvas::requestRepaint()` 在 `hasSnap=0` / `dirty=1` 状态下仍允许对子控件局部 `draw()`。
|
||||
- 页签切换期间尺寸变化会触发丢背景快照和标脏。
|
||||
- 最近相关改动:[可选] `Control::setIsVisible(true)` 变为立即请求重绘。
|
||||
|
||||
## 跟踪信息
|
||||
|
||||
- 首次发现时间: 2026-01-09
|
||||
- 最后更新时间: 2026-09-19(从 PDF 整理入库)
|
||||
- 修复版本: v3.0.0
|
||||
- 验证版本:[可选] v3.0.0
|
||||
- 备注:[可选] 来源原始 ID 为 `BUG-20260109`;同日另有 TextBox 生命周期问题,入库时按顺序拆为 `0001` 与 `0002`。
|
||||
@@ -0,0 +1,53 @@
|
||||
# BUG-20260122-0001
|
||||
|
||||
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: BUG-20260122-0001
|
||||
- 标题: Window::runEventLoop 普通控件事件分发顺序
|
||||
- 状态:已修复
|
||||
- 严重性:S3
|
||||
- 优先级:P2
|
||||
- 模块: Window
|
||||
- 版本 / 分支: v3.0.0
|
||||
- 环境: Windows 10/11 x64 + Visual Studio 2022(MSVC)Debug / Release
|
||||
- 发现人: 用户
|
||||
- 关联 Fix ID: Fix-BUG-20260122-0001
|
||||
|
||||
## 问题描述
|
||||
|
||||
- 现象: `Window::runEventLoop()` 对普通控件分发事件时采用正序遍历 `controls`,控件重叠时下层控件可能抢走点击 / 悬停事件,导致行为与绘制顺序不一致。
|
||||
- 影响范围: 普通控件存在重叠时,事件命中顺序可能不符合 Z 序直觉。
|
||||
- 期望结果: 上层控件优先接收事件,下层控件不应抢走事件。
|
||||
- 实际结果: 正序遍历导致下层控件可能先处理事件。
|
||||
|
||||
## 复现信息
|
||||
|
||||
- 前置条件:[可选] Window 中存在相互重叠的普通控件。
|
||||
- 复现步骤:
|
||||
|
||||
1. 创建存在重叠区域的普通控件。
|
||||
2. 在重叠区域触发点击或悬停事件。
|
||||
3. 观察是否由下层控件优先处理事件。
|
||||
|
||||
- 复现概率:必现(按 PDF 记录;实际复现未提供)
|
||||
- 最小复现 Demo:[可选] 无
|
||||
- 证据:PDF 历史记录指出该问题来自之前版本误将逆序分发改为正序分发。
|
||||
|
||||
## 初步分析
|
||||
|
||||
- 疑似位置: `Window::runEventLoop()`
|
||||
- 触发条件: 普通控件重叠,且事件落在重叠区域。
|
||||
- 相关线索:
|
||||
- 普通控件绘制顺序与事件分发顺序应保持 Z 序一致。
|
||||
- `Window::controls` 正序遍历会让较早加入的下层控件先处理事件。
|
||||
- 最近相关改动:[可选] 某个版本中普通控件遍历顺序被错误修改。
|
||||
|
||||
## 跟踪信息
|
||||
|
||||
- 首次发现时间: 2026-01-22
|
||||
- 最后更新时间: 2026-09-19(从 PDF 整理入库)
|
||||
- 修复版本: v3.0.1
|
||||
- 验证版本:[可选] v3.0.1
|
||||
- 备注:[可选] 来源原始 ID 为 `BUG-20260122`;PDF 未包含 `-XXXX` 后缀,入库时补充序号。
|
||||
@@ -0,0 +1,53 @@
|
||||
# BUG-20260203-0001
|
||||
|
||||
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: BUG-20260203-0001
|
||||
- 标题: 对话框打开时错误下发合成 WM_MOUSEMOVE
|
||||
- 状态:已修复
|
||||
- 严重性:S3
|
||||
- 优先级:P2
|
||||
- 模块: Window
|
||||
- 版本 / 分支: v3.0.0
|
||||
- 环境: Windows 10/11 x64 + Visual Studio 2022(MSVC)Debug / Release
|
||||
- 发现人: 用户
|
||||
- 关联 Fix ID: Fix-BUG-20260203-0001
|
||||
|
||||
## 问题描述
|
||||
|
||||
- 现象: 对话框打开时会触发本应只在对话框关闭时执行的合成 `WM_MOUSEMOVE`,并下发给所有可见控件以刷新 hover 状态。
|
||||
- 影响范围: 对话框打开路径产生额外事件下发,带来性能影响和不必要的 hover 状态刷新。
|
||||
- 期望结果: 合成 `WM_MOUSEMOVE` 只在对话框被关闭后下发一次。
|
||||
- 实际结果: 对话框打开和关闭时都会下发合成 `WM_MOUSEMOVE`。
|
||||
|
||||
## 复现信息
|
||||
|
||||
- 前置条件:[可选] 存在可弹出的对话框。
|
||||
- 复现步骤:
|
||||
|
||||
1. 启动事件循环。
|
||||
2. 触发对话框弹出。
|
||||
3. 观察日志中是否在对话框打开阶段出现合成 `WM_MOUSEMOVE` 下发记录。
|
||||
|
||||
- 复现概率:必现
|
||||
- 最小复现 Demo:[可选] 弹出一个对话框即可。
|
||||
- 证据:PDF 日志显示对话框弹出后出现“对话框关闭,合成WM_MOUSEMOVE已下发”。
|
||||
|
||||
## 初步分析
|
||||
|
||||
- 疑似位置: `Window::runEventLoop()`
|
||||
- 触发条件: 对话框打开路径触发重绘判断时,也执行了对话框关闭后才需要的鼠标移动合成逻辑。
|
||||
- 相关线索:
|
||||
- 相关变量为 `dialogClose`。
|
||||
- 合成 `WM_MOUSEMOVE` 的执行缺少对 `dialogClose` 为真的限定。
|
||||
- 最近相关改动:[可选]
|
||||
|
||||
## 跟踪信息
|
||||
|
||||
- 首次发现时间: 2026-02-03
|
||||
- 最后更新时间: 2026-09-19(从 PDF 整理入库)
|
||||
- 修复版本: v3.0.1
|
||||
- 验证版本:[可选] v3.0.1
|
||||
- 备注:[可选] 来源原始 ID 为 `BUG-20260203`;PDF 未包含 `-XXXX` 后缀,入库时补充序号。
|
||||
@@ -0,0 +1,53 @@
|
||||
# BUG-20260206-0001
|
||||
|
||||
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: BUG-20260206-0001
|
||||
- 标题: 对话框打开时持续触发全量重绘
|
||||
- 状态:已修复
|
||||
- 严重性:S3
|
||||
- 优先级:P2
|
||||
- 模块: Window
|
||||
- 版本 / 分支: v3.0.0 / master
|
||||
- 环境: Windows 10/11 x64 + Visual Studio 2022(MSVC)Debug / Release
|
||||
- 发现人: 用户
|
||||
- 关联 Fix ID: Fix-BUG-20260206-0001
|
||||
|
||||
## 问题描述
|
||||
|
||||
- 现象: 对话框保持打开时持续触发全量重绘,重复重绘所有打开的对话框和可见控件。
|
||||
- 影响范围: 对话框打开期间产生不必要的全量绘制,造成性能影响。
|
||||
- 期望结果: 只在对话框打开或关闭时触发必要重绘,确保对话框顶层显示并清除关闭残留。
|
||||
- 实际结果: 重绘前以“是否有对话框可见或关闭”为依据,导致对话框保持可见时持续触发重绘。
|
||||
|
||||
## 复现信息
|
||||
|
||||
- 前置条件:[可选] 存在可弹出的对话框。
|
||||
- 复现步骤:
|
||||
|
||||
1. 触发对话框弹出。
|
||||
2. 保持对话框打开。
|
||||
3. 观察日志中是否持续输出全量重绘相关记录。
|
||||
|
||||
- 复现概率:必现
|
||||
- 最小复现 Demo:触发任意对话框即可。
|
||||
- 证据:PDF 日志中同一时间片持续出现“对话框打开/关闭,触发全量重绘”。
|
||||
|
||||
## 初步分析
|
||||
|
||||
- 疑似位置: `Window::runEventLoop()`
|
||||
- 触发条件: 对话框处于可见状态且事件循环持续运行。
|
||||
- 相关线索:
|
||||
- 相关变量为 `needredraw`。
|
||||
- 重绘判断基于“是否存在可见对话框”,而不是“是否刚刚打开 / 关闭对话框”。
|
||||
- 最近相关改动:[可选]
|
||||
|
||||
## 跟踪信息
|
||||
|
||||
- 首次发现时间: 2026-02-06
|
||||
- 最后更新时间: 2026-09-19(从 PDF 整理入库)
|
||||
- 修复版本: v3.0.1
|
||||
- 验证版本:[可选] v3.0.1
|
||||
- 备注:[可选] 来源原始 ID 为 `BUG-20260206`;PDF 未包含 `-XXXX` 后缀,入库时补充序号。
|
||||
@@ -0,0 +1,54 @@
|
||||
# BUG-20260217-0001
|
||||
|
||||
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: BUG-20260217-0001
|
||||
- 标题: TabControl::getActiveIndex 无激活页返回最后索引
|
||||
- 状态:已修复
|
||||
- 严重性:S2
|
||||
- 优先级:P1
|
||||
- 模块: TabControl
|
||||
- 版本 / 分支: v3.0.0 / master
|
||||
- 环境: Windows 10/11 x64 + Visual Studio 2022(MSVC)Debug / Release
|
||||
- 发现人: 用户
|
||||
- 关联 Fix ID: Fix-BUG-20260217-0001
|
||||
|
||||
## 问题描述
|
||||
|
||||
- 现象: 当 TabControl 的页签对列表不为空,但所有页签均未激活 / 未打开时,调用 `getActiveIndex()` 会返回最后一个页签的索引。
|
||||
- 影响范围: 用户通过 `getActiveIndex()` 判断当前激活页时,可能拿到错误索引。
|
||||
- 期望结果: 无页签激活时返回 `-1`;有页签激活时返回对应索引。
|
||||
- 实际结果: 无页签激活时返回最后一个页签索引。
|
||||
|
||||
## 复现信息
|
||||
|
||||
- 前置条件:[可选] TabControl 页签列表不为空,但所有页签都未激活。
|
||||
- 复现步骤:
|
||||
|
||||
1. 创建含有页签的 TabControl。
|
||||
2. 保持所有页签未激活 / 未打开。
|
||||
3. 调用 `TabControl::getActiveIndex()`。
|
||||
4. 观察返回值。
|
||||
|
||||
- 复现概率:必现
|
||||
- 最小复现 Demo:[可选]
|
||||
- 证据:PDF 历史记录标记为代码分析发现。
|
||||
|
||||
## 初步分析
|
||||
|
||||
- 疑似位置: `TabControl::getActiveIndex()`
|
||||
- 触发条件: 页签列表非空且无任何页签处于激活状态。
|
||||
- 相关线索:
|
||||
- 旧逻辑未处理“列表非空但全部未激活”的兜底分支。
|
||||
- 返回值应能区分无激活页与最后一页激活。
|
||||
- 最近相关改动:[可选]
|
||||
|
||||
## 跟踪信息
|
||||
|
||||
- 首次发现时间: 2026-02-17
|
||||
- 最后更新时间: 2026-09-19(从 PDF 整理入库)
|
||||
- 修复版本: v3.0.1
|
||||
- 验证版本:[可选] v3.0.1
|
||||
- 备注:[可选] 来源原始 ID 为 `BUG-20260217`;PDF 未包含 `-XXXX` 后缀,入库时补充序号。
|
||||
@@ -48,7 +48,7 @@
|
||||
## 初步分析
|
||||
|
||||
- 疑似位置:
|
||||
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp) 的 `runEventLoop()` resize 收口逻辑
|
||||
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Window.cpp) 的 `runEventLoop()` resize 收口逻辑
|
||||
- 可疑代码特征:
|
||||
- 使用 `abs(finalW - width) > 1000 || abs(finalH - height) > 1000` 作为跳过条件
|
||||
- 命中后直接:
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
## 初步分析
|
||||
|
||||
- 疑似位置:
|
||||
- [`Button.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Button.cpp)
|
||||
- [`Button.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Button.cpp)
|
||||
- `Button::hideTooltip()`
|
||||
- 触发条件:
|
||||
- Tooltip 已经显示
|
||||
|
||||
@@ -50,9 +50,9 @@
|
||||
## 初步分析
|
||||
|
||||
- 疑似位置:
|
||||
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp)
|
||||
- [`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp)
|
||||
- [`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp)
|
||||
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Window.cpp)
|
||||
- [`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Canvas.cpp)
|
||||
- [`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/TabControl.cpp)
|
||||
- 触发条件:
|
||||
- 局部重绘提交写入了下层像素,但没有做 overlay 兄弟补画
|
||||
- 相关线索:
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# BUG-20260929-0001
|
||||
|
||||
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: BUG-20260929-0001
|
||||
- 标题: Table 运行期重置后页码标签被旧分页按钮快照覆盖
|
||||
- 状态:已修复
|
||||
- 严重性:S2
|
||||
- 优先级:P1
|
||||
- 模块: Table / Label / Button / 背景快照
|
||||
- 版本 / 分支: 当前工作区
|
||||
- 环境: Windows / EasyX / KEY1
|
||||
- 发现人: 用户
|
||||
- 关联 Fix ID: Fix-BUG-20260929-0001
|
||||
|
||||
## 问题描述
|
||||
|
||||
- 现象: KEY1 页签 1 中点击页内按钮重置表头和数据后,分页区中间的页码标签只显示一部分;触发表格重绘或与其他页签按钮进行 hover 交互后恢复完整。
|
||||
- 影响范围: Table 运行期数据变化导致表格宽度及分页控件位置同时变化的场景。
|
||||
- 期望结果: 重置后的首帧立即完整显示新页码,不依赖后续 hover、翻页或页签重绘修复。
|
||||
- 实际结果: 页码已经绘制,但随后移动的分页按钮回贴旧位置背景快照,覆盖了新页码的一部分。
|
||||
|
||||
## 复现信息
|
||||
|
||||
- 前置条件: 启动 KEY1 并进入页签 1。
|
||||
- 复现步骤:
|
||||
|
||||
1. 点击页签 1 内用于重置表格的“按钮”。
|
||||
2. 不移动鼠标,立即观察“上一页”和“下一页”之间的页码标签。
|
||||
3. 对表格或页签 2 按钮进行 hover 交互,触发后续重绘。
|
||||
|
||||
- 复现概率:稳定复现
|
||||
- 最小复现 Demo:KEY1 页签 1
|
||||
- 证据:用户截图中重置后的页码标签首帧显示不全,后续重绘恢复。
|
||||
|
||||
## 初步分析
|
||||
|
||||
- 疑似位置: `Table::drawPageNum()`、`Table::drawButton()` 和内部控件 `Control::setX()` 的调用顺序。
|
||||
- 触发条件: 新旧表格宽度不同,分页 Label 和分页按钮需要同时横向移动。
|
||||
- 相关线索:
|
||||
- 旧顺序先绘制页码,再在 `drawButton()` 中移动分页按钮。
|
||||
- 分页按钮移动时会通过 `discardBackground()` 回贴旧背景。
|
||||
- 旧按钮区域与新页码区域相交,导致新页码被局部擦除。
|
||||
- 最近相关改动:[可选] Table 页码强制设脏和分页按钮整表重绘链已经存在,但未覆盖内部控件同帧搬移后的旧快照覆盖。
|
||||
|
||||
## 跟踪信息
|
||||
|
||||
- 首次发现时间: 2026-09-29
|
||||
- 最后更新时间: 2026-09-30
|
||||
- 修复版本: 当前工作区
|
||||
- 验证版本: 2026-09-29 非 UI 全矩阵通过;2026-09-30 用户完成 KEY1 至 KEY6 手工回归,暂未发现明显问题
|
||||
- 备注:[可选]
|
||||
@@ -0,0 +1,64 @@
|
||||
# Fix-BUG-20251209-0001
|
||||
|
||||
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
|
||||
|
||||
## 关联信息
|
||||
|
||||
- Fix ID: Fix-BUG-20251209-0001
|
||||
- 关联 BUG ID: BUG-20251209-0001
|
||||
- 修复目标: TabControl 由不可见变为可见时,仅绘制激活页;无激活页时不绘制任何页,避免页面重叠和脏帧。
|
||||
- 状态:已完成
|
||||
- 负责人: 未记录
|
||||
- 分支 / 版本: master / v2.3.2
|
||||
|
||||
## 根因分析
|
||||
|
||||
- 根因: `TabControl::setIsVisible(true)` 对所有页签 / 页面统一传播 true 并置脏,导致恢复可见后本应不可绘制的非激活页也进入绘制流程。
|
||||
- 触发条件: 执行 `setIsVisible(false)` 后再执行 `setIsVisible(true)`。
|
||||
- 为什么之前没发现:[可选] 缺少“隐藏 -> 显示”回归用例,尤其缺少“多个页面但只允许激活页绘制”的断言。
|
||||
- 关键证据:[可选] PDF 历史记录明确将疑似位置定位到 `TabControl::setIsVisible()` 与 `show` 状态传播。
|
||||
|
||||
## 修复方案
|
||||
|
||||
- 修复思路: TabControl 可见性向页面传播时按激活状态过滤;显示时只允许激活页可见,隐藏时所有页都不可见。
|
||||
- 关键改动:
|
||||
- `TabControl.cpp::setIsVisible()` 在参数为 true 时,根据激活页判定对子页面传递 true / false。
|
||||
- 参数为 false 时,对所有页面传递 false。
|
||||
- 绘制入口可选择增加“仅绘制激活页”的防御性约束。
|
||||
- 涉及文件 / 类 / 函数:
|
||||
- `TabControl.cpp`
|
||||
- `TabControl::setIsVisible()`
|
||||
- 影响的 API / 行为:[可选] 对外 API 无变化;行为层面修正错误绘制。
|
||||
- 关键约束 / 不变量:[可选] 任意时刻可参与绘制的页面集合最多只有一个激活页。
|
||||
- 回滚点 / 开关:[可选] 回退 TabControl 页面可见性传播逻辑。
|
||||
|
||||
## 影响评估
|
||||
|
||||
- 影响范围: TabControl 隐藏 / 显示、页面激活状态、页面绘制链路。
|
||||
- 兼容性影响:无(纠错式兼容)
|
||||
- 行为变化:有(非激活页不再随 TabControl 显示而被绘制)
|
||||
- 性能影响:无负面影响,减少非激活页绘制后预期更优或持平。
|
||||
- 回归风险:
|
||||
- 如果激活判定逻辑异常导致“无激活页”,恢复显示后会表现为空白页;该行为符合修复目标。
|
||||
- 如果外部代码错误依赖非激活页仍可见,可能观察到行为差异。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 验证步骤:
|
||||
|
||||
1. 创建含不少于 2 个页的 TabControl,并设置激活页为 0。
|
||||
2. 调用 `setIsVisible(false)`,再通过事件调用 `setIsVisible(true)`。
|
||||
3. 确认仅激活页显示,非激活页不绘制、不重叠。
|
||||
4. 切换激活页后重复隐藏 / 显示,确认仍只绘制当前激活页。
|
||||
5. 验证无激活页场景,确认不绘制任何页。
|
||||
|
||||
- 验证结果: PDF 历史记录标记已修复。
|
||||
- 回归检查:[可选] 嵌套容器可见性、裁剪、鼠标事件命中、布局刷新、脏标记 / 快照重绘路径。
|
||||
- 验证证据:[可选]
|
||||
|
||||
## 落地信息
|
||||
|
||||
- Commit: 0c1cf2938f0d6c5684eeb318f17a8443e0c8cf5a
|
||||
- PR:[可选] git
|
||||
- 发布版本: v2.3.2
|
||||
- 备注: 来源原始 Fix 记录未包含 `-XXXX` 后缀,入库时按 BUG ID 补充为 `Fix-BUG-20251209-0001`。
|
||||
@@ -0,0 +1,69 @@
|
||||
# Fix-BUG-20251220-0001
|
||||
|
||||
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
|
||||
|
||||
## 关联信息
|
||||
|
||||
- Fix ID: Fix-BUG-20251220-0001
|
||||
- 关联 BUG ID: BUG-20251220-0001
|
||||
- 修复目标: 正确支持绘制前设置默认激活页,并在首次绘制时激活对应页面;运行时调用则立即切换,且不崩溃。
|
||||
- 状态:已完成
|
||||
- 负责人: 未记录
|
||||
- 分支 / 版本: master / v2.3.2
|
||||
|
||||
## 根因分析
|
||||
|
||||
- 根因: `setActiveIndex()` 在设置默认激活时直接调用页签按钮点击回调,但绘制前页签按钮部分内部指针尚未初始化,导致空指针访问。
|
||||
- 触发条件: 绘制前调用 `TabControl::setActiveIndex()`。
|
||||
- 为什么之前没发现:[可选] 测试缺失,未覆盖绘制前设置默认激活页的生命周期场景。
|
||||
- 关键证据:[可选] 最小复现仅需创建 TabControl 并在绘制前调用 `setActiveIndex(0)`。
|
||||
|
||||
## 修复方案
|
||||
|
||||
- 修复思路: 首次绘制前调用 `setActiveIndex()` 时只记录默认激活索引,不立即调用页签按钮点击回调;在 `TabControl::draw()` 完成基础绘制后再应用默认激活。
|
||||
- 关键改动:
|
||||
- `TabControl::setActiveIndex()` 在首次绘制前仅记录默认激活页索引。
|
||||
- `TabControl::draw()` 绘制完成后检查是否为首次绘制、是否设置默认激活页、索引是否合法。
|
||||
- 新增成员 `TabControl::IsFirstDraw`,用于判断是否首次绘制。
|
||||
- 新增成员 `TabControl::defaultActivation`,用于记录默认激活索引;未设置时为 `-1`。
|
||||
- 涉及文件 / 类 / 函数:
|
||||
- `TabControl.h`
|
||||
- `TabControl.cpp`
|
||||
- `TabControl::setActiveIndex()`
|
||||
- `TabControl::draw()`
|
||||
- 影响的 API / 行为:[可选] 对外接口不变;内部延迟首次激活流程。
|
||||
- 关键约束 / 不变量:[可选]
|
||||
- 首次绘制前的默认激活只记录,不触发未初始化对象的点击回调。
|
||||
- 首次绘制后调用 `setActiveIndex()` 应立即切换激活页。
|
||||
- 回滚点 / 开关:[可选] 回退默认激活延迟应用逻辑。
|
||||
|
||||
## 影响评估
|
||||
|
||||
- 影响范围: TabControl 默认激活、首次绘制、运行时程序化切页。
|
||||
- 兼容性影响:无(对外接口不变)
|
||||
- 行为变化:有(绘制前设置默认激活页改为首次绘制后应用)
|
||||
- 性能影响:无明显影响。
|
||||
- 回归风险:
|
||||
- 默认激活索引越界时的行为需要与既有预期保持一致。
|
||||
- 开关模式下页签按钮选中态需要与激活页同步。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 验证步骤:
|
||||
|
||||
1. 创建页签数为 2 的 TabControl。
|
||||
2. 分别在绘制前调用 `setActiveIndex(0)`、`setActiveIndex(1)`、越界索引。
|
||||
3. 确认索引 0 / 1 在首次绘制时激活对应页面,按钮处于选中状态。
|
||||
4. 运行过程中通过 Button 回调调用 `setActiveIndex()`,确认立即切换激活页。
|
||||
5. 验证未调用 `setActiveIndex()` 时默认不激活任何页面。
|
||||
|
||||
- 验证结果: PDF 历史记录标记已修复。
|
||||
- 回归检查:[可选] 页签点击切换、程序化切换、默认不激活场景、开关模式选中态、页签数量变化与越界索引行为。
|
||||
- 验证证据:[可选]
|
||||
|
||||
## 落地信息
|
||||
|
||||
- Commit: 0c1cf2938f0d6c5684eeb318f17a8443e0c8cf5a
|
||||
- PR:[可选] git 推送
|
||||
- 发布版本: v2.3.2
|
||||
- 备注: 来源原始 Fix 记录未包含 `-XXXX` 后缀,入库时按 BUG ID 补充为 `Fix-BUG-20251220-0001`。
|
||||
@@ -0,0 +1,65 @@
|
||||
# Fix-BUG-20260109-0001
|
||||
|
||||
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
|
||||
|
||||
## 关联信息
|
||||
|
||||
- Fix ID: Fix-BUG-20260109-0001
|
||||
- 关联 BUG ID: BUG-20260109-0001
|
||||
- 修复目标: 修复 TextBox 在 Window 创建 / 绘制前调用 `setText()` 设置默认文本时因即时绘制导致的崩溃。
|
||||
- 状态:已完成
|
||||
- 负责人: 未记录
|
||||
- 分支 / 版本: master / v3.0.0
|
||||
|
||||
## 根因分析
|
||||
|
||||
- 根因: `TextBox::setText()` 将“状态更新”和“立即绘制”耦合在一起。窗口尚未 `initgraph()`、EasyX 图形上下文未就绪时,`draw()` 内部进入 `Control::saveBackground()`,调用 `SetWorkingImage()` / `getimage()` 等依赖有效图形环境的逻辑,最终崩溃。
|
||||
- 触发条件: 创建 TextBox 后,在 Window 初始化 / 首次绘制前调用 `TextBox::setText()`。
|
||||
- 为什么之前没发现:[可选] 常规示例多为 Window 完成初始化后再设置文本,缺少控件生命周期前置调用的最小回归用例。
|
||||
- 关键证据:[可选] 调用链为 `TextBox::setText()` -> `TextBox::draw()` -> `Control::saveBackground()` -> EasyX 图形上下文相关 API。
|
||||
|
||||
## 修复方案
|
||||
|
||||
- 修复思路: `TextBox::setText()` 改为仅执行文本赋值和脏标记更新,不再在 setter 内直接调用 `draw()`;绘制统一交给 Window / 容器重绘流程。
|
||||
- 关键改动:
|
||||
- 移除 `TextBox::setText(std::string text)` 中的直接 `draw()` 调用。
|
||||
- 保留长度裁剪、文本赋值、`dirty=true`。
|
||||
- 将可视更新延后到后续统一重绘周期。
|
||||
- 涉及文件 / 类 / 函数:
|
||||
- `TextBox.cpp`
|
||||
- `TextBox::setText()`
|
||||
- 影响的 API / 行为:[可选] 对外 API 不变;`setText()` 不再保证即时绘制。
|
||||
- 关键约束 / 不变量:[可选]
|
||||
- setter 不得直接调用 `draw()` 或任何依赖图形上下文的 API。
|
||||
- `saveBackground()` / `getimage()` / `putimage()` / `SetWorkingImage()` 只能在 Window 图形上下文就绪并进入统一绘制流程后执行。
|
||||
- 回滚点 / 开关:[可选] 回退 setter 内即时绘制逻辑。
|
||||
|
||||
## 影响评估
|
||||
|
||||
- 影响范围: TextBox 默认文本设置、程序化文本更新、绘制时序。
|
||||
- 兼容性影响:有(如果外部代码依赖 `setText()` 的即时绘制副作用,需要调整为触发一次重绘或等待绘制周期)
|
||||
- 行为变化:有(`setText()` 语义收敛为更新状态并标脏)
|
||||
- 性能影响:正向或无影响,减少 setter 内重复绘制与抓屏开销。
|
||||
- 回归风险:
|
||||
- 程序化调用 `setText()` 后,如果应用没有触发任何重绘路径,界面更新可能延迟。
|
||||
- 依赖即时刷新语义的旧代码可能出现短暂视觉滞后。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 验证步骤:
|
||||
|
||||
1. 构造最小复现:在 Window 创建 / `draw()` 前创建 TextBox 并调用 `setText("default")`,确认不再崩溃。
|
||||
2. Window 初始化后,通过事件输入或直接调用 `setText()` 更新文本,确认后续一次重绘时文本正确显示。
|
||||
3. 覆盖 INPUT / READONLY / PASSWORD 三种模式,确认长度裁剪与显示逻辑不受影响。
|
||||
4. 覆盖窗口 resize,确认背景快照机制仍按既有逻辑工作,无残影或错位。
|
||||
|
||||
- 验证结果: PDF 历史记录标记已修复。
|
||||
- 回归检查:[可选] TextBox 三模式输入与显示、背景快照保存 / 恢复 / 丢弃、Window 首次绘制、对话框打开 / 关闭强制重绘、resize 收口重绘。
|
||||
- 验证证据:[可选]
|
||||
|
||||
## 落地信息
|
||||
|
||||
- Commit: 5cb59b36525cde93de83313551296c6458694110
|
||||
- PR:[可选] git
|
||||
- 发布版本: v3.0.0
|
||||
- 备注: 来源原始 Fix 记录未包含 `-XXXX` 后缀,入库时按 BUG ID 补充为 `Fix-BUG-20260109-0001`。
|
||||
@@ -0,0 +1,69 @@
|
||||
# Fix-BUG-20260109-0002
|
||||
|
||||
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
|
||||
|
||||
## 关联信息
|
||||
|
||||
- Fix ID: Fix-BUG-20260109-0002
|
||||
- 关联 BUG ID: BUG-20260109-0002
|
||||
- 修复目标: 修复 TabControl 页签切换与 Table 重置数据后出现的新旧内容像素叠加,确保快照机制不被污染。
|
||||
- 状态:已完成
|
||||
- 负责人: 未记录
|
||||
- 分支 / 版本: master / v3.0.0
|
||||
|
||||
## 根因分析
|
||||
|
||||
- 根因: `Control::setIsVisible(true)` 立即向上 `requestRepaint()`,导致页签切换 / 关闭过程中,父容器可能处于 `hasSnap=0`、`dirty=1` 或刚 `discardBackground()` 的快照未就绪状态。此时 `Canvas::requestRepaint(partial)` 直接对子控件 Table 调用 `draw()`,随后 `saveBackground()` 又把已经包含旧表格像素的屏幕区域截入快照,形成背景快照污染。
|
||||
- 触发条件: 切换到按钮1页签 -> 重置表格数据 -> 切换 / 关闭其他页签 -> 再切回按钮1;或重置数据当次容器快照未就绪时触发局部重绘。
|
||||
- 为什么之前没发现:[可选] 历史版本 `setIsVisible(true)` 不会立即 `requestRepaint()`;常规交互未必同时满足页签切换 / 表格重置 / Null 填充 / 透明文字背景的组合条件。
|
||||
- 关键证据:[可选] 日志中出现 Canvas 丢弃快照后,Table `show=1` 立即触发局部重绘,随后保存 Table 背景快照。
|
||||
|
||||
## 修复方案
|
||||
|
||||
- 修复思路: 回退 `show=true` 立即请求重绘的行为,并为 Canvas / TabControl 的局部重绘增加快照就绪护栏;当容器快照未就绪或 dirty 未清理时,禁止 partial fast-path,降级为 full draw。
|
||||
- 关键改动:
|
||||
- `Control::setIsVisible()` 中 `show=true` 时不再立即 `requestRepaint()`,改为仅置脏;`show=false` 保持擦除逻辑。
|
||||
- `Canvas::requestRepaint()` 增加护栏,`dirty` / 无快照 / 无缓存图时将 partial 降级为 full draw。
|
||||
- `TabControl::requestRepaint()` 同步增加局部重绘护栏。
|
||||
- 可选修正 `Table::setData(vector<string>)` 列补齐 off-by-one,降低潜在越界或异常噪声。
|
||||
- 涉及文件 / 类 / 函数:
|
||||
- `Control.cpp`
|
||||
- `Canvas.cpp`
|
||||
- `TabControl.cpp`
|
||||
- `Table::setData()`(可选)
|
||||
- 影响的 API / 行为:[可选] 对外 API 不变;显示时序与局部重绘降级策略发生内部调整。
|
||||
- 关键约束 / 不变量:[可选]
|
||||
- 局部重绘前,父容器必须持有有效快照且不处于 dirty 未清理状态。
|
||||
- `setIsVisible(true)` 不得在快照未就绪阶段触发子控件抢跑绘制。
|
||||
- 回滚点 / 开关:[可选] 回退 visible 置脏与 partial 降级护栏。
|
||||
|
||||
## 影响评估
|
||||
|
||||
- 影响范围: TabControl 页签切换、Canvas 局部重绘、Table 数据重置、背景快照捕获。
|
||||
- 兼容性影响:无 API 变化;若外部依赖 `show=true` 即刻刷新,需要确保后续绘制收口会触发重绘。
|
||||
- 行为变化:有(快照未就绪时局部重绘降级为全量重绘)
|
||||
- 性能影响:在快照未就绪阶段单次重绘成本增加;稳定交互阶段仍保留局部重绘路径。
|
||||
- 回归风险:
|
||||
- 降级条件过宽可能导致频繁全量重绘。
|
||||
- 显示状态更新后,如果外部没有触发任何重绘,可能存在视觉延迟。
|
||||
- 程序化页签激活流程可能受重绘时序影响,需要回归。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 验证步骤:
|
||||
|
||||
1. 按稳定复现路径:按钮1 -> 重置表格数据 -> 切换 / 关闭其他页签 -> 切回按钮1,重复 50 次以上。
|
||||
2. 在按钮1停留时连续快速点击重置 100 次,并穿插页签切换、窗口最小化 / 恢复、拖拽 / resize。
|
||||
3. 反复对 Table / Canvas 调用 `setIsVisible(false / true)`,确认隐藏擦除正确、显示后无残影。
|
||||
4. 分别在 TableFillMode 为 Null 与非 Null 模式下验证。
|
||||
|
||||
- 验证结果: PDF 历史记录标记已修复。
|
||||
- 回归检查:[可选] TabControl 切换 / 关闭 / 重复打开、Canvas 添加 / 移除控件与 resize、Table clearData / setData / 翻页、全局窗口 resize / 最小化恢复。
|
||||
- 验证证据:[可选] PDF 记录中关联 `QQ202619-144430.mp4`。
|
||||
|
||||
## 落地信息
|
||||
|
||||
- Commit: 5cb59b36525cde93de83313551296c6458694110
|
||||
- PR:[可选] git 推送
|
||||
- 发布版本: v3.0.0
|
||||
- 备注: 来源原始 Fix 记录未包含 `-XXXX` 后缀,入库时按 BUG ID 补充为 `Fix-BUG-20260109-0002`。
|
||||
@@ -0,0 +1,58 @@
|
||||
# Fix-BUG-20260122-0001
|
||||
|
||||
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
|
||||
|
||||
## 关联信息
|
||||
|
||||
- Fix ID: Fix-BUG-20260122-0001
|
||||
- 关联 BUG ID: BUG-20260122-0001
|
||||
- 修复目标: 将 `Window::runEventLoop()` 中普通控件事件分发顺序由正序遍历改为逆序遍历,以保持 Z 序直觉。
|
||||
- 状态:已完成
|
||||
- 负责人: 未记录
|
||||
- 分支 / 版本: master / v3.0.1
|
||||
|
||||
## 根因分析
|
||||
|
||||
- 根因: 之前版本修改 `Window::runEventLoop()` 时,将原本的逆序事件分发错误改成了正序遍历。
|
||||
- 触发条件: 普通控件重叠时,事件落在多个控件的命中区域。
|
||||
- 为什么之前没发现:[可选] 测试缺失,提交时未进行详细代码审查。
|
||||
- 关键证据:[可选] PDF 历史记录说明该问题属于代码审查发现的遍历顺序错误。
|
||||
|
||||
## 修复方案
|
||||
|
||||
- 修复思路: 对 `controls` 使用逆序遍历,让绘制顺序更靠上的控件优先处理事件。
|
||||
- 关键改动:
|
||||
- 将 `for (auto& c : controls)` 调整为 `for (auto it = controls.rbegin(); it != controls.rend(); ++it)`。
|
||||
- 涉及文件 / 类 / 函数:
|
||||
- `Window.cpp`
|
||||
- `Window::runEventLoop()`
|
||||
- 影响的 API / 行为:[可选] 无对外 API 变化;重叠控件事件命中顺序修正。
|
||||
- 关键约束 / 不变量:[可选] 普通控件事件分发应保持逆序遍历,避免下层控件抢事件。
|
||||
- 回滚点 / 开关:[可选] 回退普通控件事件遍历顺序。
|
||||
|
||||
## 影响评估
|
||||
|
||||
- 影响范围: 普通控件点击、悬停、重叠控件事件命中。
|
||||
- 兼容性影响:无(内部逻辑修改)
|
||||
- 行为变化:有(重叠控件由上层优先处理事件)
|
||||
- 性能影响:无明显影响。
|
||||
- 回归风险: 控件重叠时的事件分发行为变化,需要确认符合绘制 Z 序。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 验证步骤:
|
||||
|
||||
1. 创建重叠普通控件。
|
||||
2. 在重叠区域触发点击与悬停。
|
||||
3. 确认上层控件优先收到事件。
|
||||
|
||||
- 验证结果: PDF 历史记录标记已修复;原记录未提供详细验证证据。
|
||||
- 回归检查:[可选] 普通控件事件分发、重叠按钮点击、悬停状态变化。
|
||||
- 验证证据:[可选] 无
|
||||
|
||||
## 落地信息
|
||||
|
||||
- Commit: b765e54eff28ebcd0bd99a81dec80b3ac0769337
|
||||
- PR:[可选] git 推送
|
||||
- 发布版本: v3.0.1
|
||||
- 备注: PDF 原文合入分支写作 `mster`,本次按仓库语境整理为 `master`。
|
||||
@@ -0,0 +1,59 @@
|
||||
# Fix-BUG-20260203-0001
|
||||
|
||||
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
|
||||
|
||||
## 关联信息
|
||||
|
||||
- Fix ID: Fix-BUG-20260203-0001
|
||||
- 关联 BUG ID: BUG-20260203-0001
|
||||
- 修复目标: 避免对话框打开时错误合成并下发 `WM_MOUSEMOVE`,只在对话框关闭后刷新可见控件 hover 状态。
|
||||
- 状态:已完成
|
||||
- 负责人: 未记录
|
||||
- 分支 / 版本: master / v3.0.1
|
||||
|
||||
## 根因分析
|
||||
|
||||
- 根因: 合成 `WM_MOUSEMOVE` 的代码位于 `if (needredraw || dialogClose)` 范围内,但执行合成下发时没有再判断 `dialogClose` 是否为真。
|
||||
- 触发条件: 打开对话框触发重绘判断。
|
||||
- 为什么之前没发现:[可选] 早期未引入日志系统,隐藏的事件下发问题不易被发现。
|
||||
- 关键证据:[可选] 对话框打开日志中出现“对话框关闭,合成WM_MOUSEMOVE已下发”。
|
||||
|
||||
## 修复方案
|
||||
|
||||
- 修复思路: 合成 `WM_MOUSEMOVE` 前显式判断 `dialogClose`,只有对话框关闭路径才执行合成并下发。
|
||||
- 关键改动:
|
||||
- 在 `Window::runEventLoop()` 中,对合成 `WM_MOUSEMOVE` 下发逻辑增加 `dialogClose == true` 条件。
|
||||
- 涉及文件 / 类 / 函数:
|
||||
- `Window.cpp`
|
||||
- `Window::runEventLoop()`
|
||||
- 影响的 API / 行为:[可选] 无对外 API 变化。
|
||||
- 关键约束 / 不变量:[可选] `dialogClose == true` 才能合成并下发 `WM_MOUSEMOVE`。
|
||||
- 回滚点 / 开关:[可选] 回退 `dialogClose` 条件判断。
|
||||
|
||||
## 影响评估
|
||||
|
||||
- 影响范围: 对话框打开 / 关闭后的 hover 状态刷新、普通控件事件下发。
|
||||
- 兼容性影响:无
|
||||
- 行为变化:有(对话框打开阶段不再下发合成 `WM_MOUSEMOVE`)
|
||||
- 性能影响:持平或更优,减少不必要事件下发。
|
||||
- 回归风险: 暂无;需确认对话框关闭后 hover 状态仍能正确刷新。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 验证步骤:
|
||||
|
||||
1. 创建包含对话框的最小 Demo。
|
||||
2. 启动日志系统。
|
||||
3. 触发对话框弹出并观察日志。
|
||||
4. 关闭对话框并确认只在关闭阶段出现合成 `WM_MOUSEMOVE` 下发。
|
||||
|
||||
- 验证结果: PDF 历史记录标记已修复。
|
||||
- 回归检查:[可选] 非模态对话框事件处理、关闭后 hover 状态刷新、普通控件悬停状态。
|
||||
- 验证证据:[可选] PDF 记录包含 2026-02-10 运行日志。
|
||||
|
||||
## 落地信息
|
||||
|
||||
- Commit: b765e54eff28ebcd0bd99a81dec80b3ac0769337
|
||||
- PR:[可选] git 推送
|
||||
- 发布版本: v3.0.1
|
||||
- 备注: 来源原始 Fix 记录未包含 `-XXXX` 后缀,入库时按 BUG ID 补充为 `Fix-BUG-20260203-0001`。
|
||||
@@ -0,0 +1,63 @@
|
||||
# Fix-BUG-20260206-0001
|
||||
|
||||
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
|
||||
|
||||
## 关联信息
|
||||
|
||||
- Fix ID: Fix-BUG-20260206-0001
|
||||
- 关联 BUG ID: BUG-20260206-0001
|
||||
- 修复目标: 只在对话框打开或关闭时触发一次必要全量重绘,避免对话框保持打开期间持续全量重绘。
|
||||
- 状态:已完成
|
||||
- 负责人: 未记录
|
||||
- 分支 / 版本: master / v3.0.1
|
||||
|
||||
## 根因分析
|
||||
|
||||
- 根因: 对话框打开状态判断基于“是否有对话框可见”,导致只要有对话框保持打开,`needredraw` 就持续为 true。
|
||||
- 触发条件: 触发对话框打开并保持可见。
|
||||
- 为什么之前没发现:[可选] 之前没有引入日志系统,该问题属于性能类隐藏问题,不容易观察。
|
||||
- 关键证据:[可选] 日志中连续出现“对话框打开/关闭,触发全量重绘”。
|
||||
|
||||
## 修复方案
|
||||
|
||||
- 修复思路: Window 新增 `dialogOpen` 状态,只有对话框刚打开时才触发打开相关重绘;完成重绘后立即复位。
|
||||
- 关键改动:
|
||||
- `Window` 新增变量 `dialogOpen`。
|
||||
- 对话框弹出时设置 `dialogOpen=true`。
|
||||
- `needredraw` 判断逻辑改为只在 `dialogOpen` 或关闭事件发生时触发。
|
||||
- 重绘后立即将 `dialogOpen` 复位为 false。
|
||||
- 涉及文件 / 类 / 函数:
|
||||
- `Window.cpp`
|
||||
- `Dialog.cpp`
|
||||
- `Window::runEventLoop()`
|
||||
- 影响的 API / 行为:[可选] 无对外 API 变化。
|
||||
- 关键约束 / 不变量:[可选] `dialogOpen == true` 才触发对话框打开相关重绘判断;重绘后必须复位。
|
||||
- 回滚点 / 开关:[可选] 回退 `dialogOpen` 状态门控。
|
||||
|
||||
## 影响评估
|
||||
|
||||
- 影响范围: 对话框打开 / 关闭后的全量重绘、主事件循环性能。
|
||||
- 兼容性影响:无
|
||||
- 行为变化:有(对话框保持打开时不再持续触发全量重绘)
|
||||
- 性能影响:持平或更优,减少重复全量绘制。
|
||||
- 回归风险: 未知;需确认对话框打开时仍能置顶并正确清理关闭残留。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 验证步骤:
|
||||
|
||||
1. 触发对话框弹出。
|
||||
2. 观察日志,确认打开时只触发一次必要全量重绘。
|
||||
3. 保持对话框打开,确认不再持续刷全量重绘日志。
|
||||
4. 关闭对话框,确认关闭路径仍能重绘清理残留。
|
||||
|
||||
- 验证结果: PDF 历史记录标记已修复。
|
||||
- 回归检查:[可选] 对话框打开 / 关闭、非模态对话框、普通控件重绘、背景快照刷新。
|
||||
- 验证证据:[可选] PDF 记录包含 2026-02-15 修复后运行日志。
|
||||
|
||||
## 落地信息
|
||||
|
||||
- Commit: b765e54eff28ebcd0bd99a81dec80b3ac0769337
|
||||
- PR:[可选] git 推送
|
||||
- 发布版本: v3.0.1
|
||||
- 备注: 来源原始 Fix 记录未包含 `-XXXX` 后缀,入库时按 BUG ID 补充为 `Fix-BUG-20260206-0001`。
|
||||
@@ -0,0 +1,58 @@
|
||||
# Fix-BUG-20260217-0001
|
||||
|
||||
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
|
||||
|
||||
## 关联信息
|
||||
|
||||
- Fix ID: Fix-BUG-20260217-0001
|
||||
- 关联 BUG ID: BUG-20260217-0001
|
||||
- 修复目标: `TabControl::getActiveIndex()` 在页签列表为空或无页签激活时返回 `-1`,有页签激活时返回对应索引。
|
||||
- 状态:已完成
|
||||
- 负责人: 未记录
|
||||
- 分支 / 版本: master / v3.0.1
|
||||
|
||||
## 根因分析
|
||||
|
||||
- 根因: `getActiveIndex()` 未处理“页签列表不为空但所有页签都未激活”的情况。
|
||||
- 触发条件: 调用 `TabControl::getActiveIndex()` 时,页签列表非空且没有任何激活页签。
|
||||
- 为什么之前没发现:[可选] 测试缺失,未覆盖无激活页但列表非空的返回值场景。
|
||||
- 关键证据:[可选] 代码分析确认旧逻辑会返回最后一个页签索引。
|
||||
|
||||
## 修复方案
|
||||
|
||||
- 修复思路: 增加无激活页兜底逻辑;页签列表为空或遍历后未找到激活页时统一返回 `-1`。
|
||||
- 关键改动:
|
||||
- 修改 `TabControl::getActiveIndex()`,在无激活页时返回 `-1`。
|
||||
- 涉及文件 / 类 / 函数:
|
||||
- `TabControl.cpp`
|
||||
- `TabControl::getActiveIndex()`
|
||||
- 影响的 API / 行为:[可选] 修正返回值语义;调用方可可靠用 `-1` 判断无激活页。
|
||||
- 关键约束 / 不变量:[可选] 无激活页必须返回 `-1`,不能返回任意有效索引。
|
||||
- 回滚点 / 开关:[可选] 回退无激活页兜底返回逻辑。
|
||||
|
||||
## 影响评估
|
||||
|
||||
- 影响范围: TabControl 激活页查询、依赖 `getActiveIndex()` 的业务判断。
|
||||
- 兼容性影响:无(修复 BUG)
|
||||
- 行为变化:有(无激活页从错误有效索引变为 `-1`)
|
||||
- 性能影响:无明显影响。
|
||||
- 回归风险: 低;需确认有激活页和空列表场景返回值正确。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 验证步骤:
|
||||
|
||||
1. 调用 `TabControl::getActiveIndex()`,验证页签列表为空时返回 `-1`。
|
||||
2. 验证页签列表不为空且所有页签未激活时返回 `-1`。
|
||||
3. 验证存在激活页时返回对应索引。
|
||||
|
||||
- 验证结果: PDF 历史记录标记已修复。
|
||||
- 回归检查:[可选] TabControl 默认激活、页签切换、无激活页状态。
|
||||
- 验证证据:[可选] 无
|
||||
|
||||
## 落地信息
|
||||
|
||||
- Commit: b765e54eff28ebcd0bd99a81dec80b3ac0769337
|
||||
- PR:[可选] git
|
||||
- 发布版本: v3.0.1
|
||||
- 备注: 来源原始 Fix 记录未包含 `-XXXX` 后缀,入库时按 BUG ID 补充为 `Fix-BUG-20260217-0001`。
|
||||
@@ -62,7 +62,7 @@
|
||||
|
||||
- 验证方式:
|
||||
1. 在 `>1000` 旧保护分支中先加 `DEBUG` 日志,确认 `KEY == 1 / 2` 命中。
|
||||
2. 替换为非法尺寸保护后,编译 [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp)。
|
||||
2. 替换为非法尺寸保护后,编译 [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Window.cpp)。
|
||||
3. 按 `KEY == 1 ~ 4` 做逻辑推演。
|
||||
- 理论结果:
|
||||
- `KEY == 1 / 2`:现在应打印“大跨度但继续执行收口”日志,并继续完成 `尺寸调整已完成`。
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
- 之前更关注 Tooltip 的显示、Hover 切换和对话框遮挡链路
|
||||
- “逻辑状态已隐藏,但屏幕区域无人回贴”的问题只有在当前托管重绘路径下才更稳定暴露
|
||||
- 关键证据:
|
||||
- [`Button.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Button.cpp) 中 `hideTooltip()` 在托管分发阶段只作废快照,不回贴 Tooltip 自身区域
|
||||
- [`Button.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Button.cpp) 中 `hideTooltip()` 在托管分发阶段只作废快照,不回贴 Tooltip 自身区域
|
||||
|
||||
## 修复方案
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
- 只要 `tipVisible == true`,统一调用 `tipLabel.hide()`
|
||||
- 保留 `tipHoverTick` 重置逻辑
|
||||
- 涉及文件 / 类 / 函数:
|
||||
- [`Button.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Button.cpp)
|
||||
- [`Button.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Button.cpp)
|
||||
- `Button::hideTooltip()`
|
||||
- 影响的 API / 行为:
|
||||
- 无公开 API 变化
|
||||
@@ -64,7 +64,7 @@
|
||||
|
||||
- 验证步骤:
|
||||
|
||||
1. 修改 [`Button.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Button.cpp) 中 `Button::hideTooltip()` 的 Tooltip 隐藏分支
|
||||
1. 修改 [`Button.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Button.cpp) 中 `Button::hideTooltip()` 的 Tooltip 隐藏分支
|
||||
2. 编译 `Button.cpp`
|
||||
3. 编译 `z-testDome.cpp` 并指定 `KEY=4`,确认 Tooltip 相关 demo 入口可正常编译
|
||||
|
||||
|
||||
@@ -34,9 +34,9 @@
|
||||
- `TabControl`:按自己的真实绘制顺序补画页签按钮与页面
|
||||
- overlay 补画前统一 `invalidateBackgroundSnapshot()`,避免旧快照反贴旧背景
|
||||
- 涉及文件 / 类 / 函数:
|
||||
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp)
|
||||
- [`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp)
|
||||
- [`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp)
|
||||
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Window.cpp)
|
||||
- [`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Canvas.cpp)
|
||||
- [`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/TabControl.cpp)
|
||||
- 影响的 API / 行为:[可选]
|
||||
- 无对外 API 变化
|
||||
- 局部重绘行为更严格遵守视觉合成顺序
|
||||
@@ -62,8 +62,8 @@
|
||||
|
||||
- 验证步骤:
|
||||
|
||||
1. 编译 [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp)、[`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp)、[`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp)
|
||||
2. 编译 [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp) `KEY=5`
|
||||
1. 编译 [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Window.cpp)、[`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Canvas.cpp)、[`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/TabControl.cpp)
|
||||
2. 编译 [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/app/z-testDome.cpp) `KEY=5`
|
||||
3. 回归 `Table` 翻页、相交 `Canvas`、`TabControl` 页签/页面遮挡场景
|
||||
|
||||
- 验证结果:
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# Fix-BUG-20260929-0001
|
||||
|
||||
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
|
||||
|
||||
## 关联信息
|
||||
|
||||
- Fix ID: Fix-BUG-20260929-0001
|
||||
- 关联 BUG ID: BUG-20260929-0001
|
||||
- 修复目标: 避免 Table 运行期重置后的分页按钮旧快照覆盖新页码
|
||||
- 状态:已完成
|
||||
- 负责人: Codex
|
||||
- 分支 / 版本: 当前工作区
|
||||
|
||||
## 根因分析
|
||||
|
||||
- 根因: `Table::draw()` 先通过 `drawPageNum()` 绘制新页码,随后 `drawButton()` 才重新定位上一页和下一页按钮。按钮的 `setX()/setY()` 会回贴旧位置背景快照;当旧按钮区域与新页码位置相交时,新页码被局部擦除。
|
||||
- 触发条件: 运行期替换表头或数据后,表格宽度和分页控件位置发生变化。
|
||||
- 为什么之前没发现: 既有检查覆盖普通翻页重绘,但没有覆盖“表格结构尺寸变化 + 三个分页控件同帧搬移”的首帧顺序。
|
||||
- 关键证据: 后续 hover 只触发重绘、不再改变分页控件位置,此时页码立即恢复完整,说明页码内容和设脏逻辑本身有效。
|
||||
|
||||
## 修复方案
|
||||
|
||||
- 修复思路: 将分页内部控件的布局刷新前移到 Table 新一帧绘制之前。先更新页码文本,再统一移动页码 Label 和两个分页按钮并清理旧快照,最后绘制表体、页码和按钮。
|
||||
- 关键改动:
|
||||
- 集中生成当前页码文本,避免布局阶段和绘制阶段使用不同字符串。
|
||||
- 当分页布局失效时,在 Table 回贴并重绘自身之前完成页码 Label 的尺寸、位置和文本更新。
|
||||
- 在同一预布局阶段完成上一页、下一页按钮的位置更新,确保旧快照不会覆盖本帧已经绘制的分页元素。
|
||||
- 保留最终“表体 -> 页码 -> 分页按钮”的绘制顺序。
|
||||
- 涉及文件 / 类 / 函数:
|
||||
- `src/StellarX/Table.cpp`
|
||||
- `Table::draw()`
|
||||
- `Table::drawPageNum()`
|
||||
- 影响的 API / 行为: 无公共 API 变化;仅修正运行期重置后的首帧绘制结果。
|
||||
- 关键约束 / 不变量: Table 内部控件因布局变化产生的旧快照回贴必须发生在父 Table 绘制新内容之前。
|
||||
- 回滚点 / 开关: 回退 `Table::draw()` 中的分页预布局块及统一页码文本生成函数。
|
||||
|
||||
## 影响评估
|
||||
|
||||
- 影响范围: Table 数据、表头、每页行数变化后的分页区重绘。
|
||||
- 兼容性影响:无
|
||||
- 行为变化:有(重置后的首帧直接显示完整页码)
|
||||
- 性能影响:可忽略,仅在分页布局失效时提前执行原有位置刷新。
|
||||
- 回归风险:
|
||||
- 透明 Table 背景下的分页区快照恢复顺序变化。
|
||||
- 页码位数变化造成 Label 宽度变化时,需要确认左右按钮仍与页码保持正确间距。
|
||||
- 隐藏分页按钮后再恢复显示时,需要确认按钮仍能初始化并正确定位。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 验证步骤:
|
||||
|
||||
1. 执行 `scripts/test.ps1 -Suite NonUI`。
|
||||
2. KEY1 页签 1 点击页内按钮后,不移动鼠标立即检查完整页码。
|
||||
3. 连续重置三次,并执行上一页、下一页切换。
|
||||
4. hover 页签 2、页签 3 并切换后返回,确认分页区无残影。
|
||||
|
||||
- 验证结果: 2026-09-29 执行 `scripts/test.ps1 -Suite NonUI`,Debug x64、Debug x64 ASan、Release x64、Debug Win32 均为 15/15 通过;ASan 未报告内存错误;KEY1 至 KEY6 均编译、链接通过。用户于 2026-09-30 完成 KEY1 至 KEY6 手工回归,暂未发现明显问题。
|
||||
- 回归检查: KEY1 表格重置与翻页;KEY5 Table 分页、页码和上层浮层。
|
||||
- 验证证据:[可选]
|
||||
|
||||
## 落地信息
|
||||
|
||||
- Commit: 未提交
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选]
|
||||
- 备注: UI 像素结果已由用户按 `TESTING.md` 完成人工验收。
|
||||
@@ -0,0 +1,74 @@
|
||||
# StellarX API 迁移说明
|
||||
|
||||
本次变更包含不兼容的所有权与文本样式 API 调整。GUI 仍限定在主线程,字符集仍为 MBCS。
|
||||
|
||||
## Window 与 Canvas 所有权
|
||||
|
||||
不再公开可变的 `std::vector<std::unique_ptr<Control>>`。调用方必须保存稳定句柄,而不是保存容器迭代器或假设控件地址永久有效。
|
||||
|
||||
```cpp
|
||||
auto button = std::make_unique<Button>(10, 10, 100, 32, "run");
|
||||
const StellarX::ControlHandle handle = window.addControl(std::move(button));
|
||||
|
||||
Control* control = window.findControl(handle);
|
||||
const bool removed = window.removeControl(handle);
|
||||
```
|
||||
|
||||
新接口:
|
||||
|
||||
- `ControlHandle addControl(std::unique_ptr<Control>)`
|
||||
- `bool removeControl(ControlHandle)`
|
||||
- `void clearControls()`
|
||||
- `Control* findControl(ControlHandle)`
|
||||
- `std::vector<ControlHandle> getControlHandles() const`
|
||||
|
||||
已删除:
|
||||
|
||||
- `Window::getControls()`
|
||||
- `Canvas::getControls()`
|
||||
- `Canvas::clearAllControls()`
|
||||
|
||||
事件、绘制、tick 和按钮回调遍历期间的新增、删除与清空会延迟到最外层事务结束后提交。回调结束后不要继续使用已请求删除的裸指针。
|
||||
|
||||
## 字体名称
|
||||
|
||||
`ControlText::lpszFace` 已替换为拥有型字符串 `ControlText::faceName`。
|
||||
|
||||
```cpp
|
||||
StellarX::ControlText style;
|
||||
style.faceName = "微软雅黑";
|
||||
```
|
||||
|
||||
不再传入临时 `c_str()` 指针。字体样式比较按字符串内容进行,复制赋值使用默认值语义。
|
||||
|
||||
## TabControl
|
||||
|
||||
页签身份改为索引,重复标题合法。
|
||||
|
||||
```cpp
|
||||
const size_t index = tabs.addTab(std::move(button), std::move(page));
|
||||
auto rejected = tabs.tryAddToPage(index, std::move(control));
|
||||
if (rejected) {
|
||||
// 添加失败,调用方仍拥有控件。
|
||||
}
|
||||
```
|
||||
|
||||
替换关系:
|
||||
|
||||
- `add(pair<Button, Canvas>)` 改为 `addTab(button, page)`,返回页签索引。
|
||||
- `add(tabText, control)` 改为 `tryAddToPage(index, control)`;文本重载只匹配第一个同名页签。
|
||||
- `setActiveIndex(int)` 现在返回 `bool`,运行期非法索引返回 `false`。
|
||||
|
||||
空 TabControl 可预设首次激活索引;首次绘制不会访问空容器。索引超过首批页签范围时首次激活最后一个可用页签。
|
||||
|
||||
## 日志配置
|
||||
|
||||
`SxLogger::setConfig` 现在返回 `bool`:
|
||||
|
||||
```cpp
|
||||
if (!StellarX::SxLogger::Get().setConfig(config)) {
|
||||
// 候选文件 sink 打开失败,旧配置和旧 sink 保持有效。
|
||||
}
|
||||
```
|
||||
|
||||
文件输出禁用、路径、追加模式或滚动阈值变化会同步更新 sink。滚动重命名失败时继续追加原文件,不再截断。
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
# StellarX 自动化测试
|
||||
|
||||
## 依赖
|
||||
|
||||
- Visual Studio 2022 MSVC v143、Windows SDK、Native Unit Test Framework。
|
||||
- EasyX 已安装到当前 MSVC 环境。
|
||||
- UI 套件需要已解锁的交互式 Windows 桌面,会短暂切换前台窗口并移动鼠标,结束后恢复鼠标位置。
|
||||
|
||||
默认工具路径为 `D:\vs2022`。其他安装位置可设置:
|
||||
|
||||
- `STELLARX_MSBUILD`:`MSBuild.exe` 的完整路径。
|
||||
- `STELLARX_VSTEST`:`vstest.console.exe` 的完整路径。
|
||||
|
||||
## 命令
|
||||
|
||||
```powershell
|
||||
.\scripts\test.ps1 -Suite Unit -Configuration Debug -Platform x64
|
||||
.\scripts\test.ps1 -Suite NonUI
|
||||
.\scripts\test.ps1 -Suite Render
|
||||
.\scripts\test.ps1 -Suite UI
|
||||
.\scripts\test.ps1 -Suite All
|
||||
.\scripts\test.ps1 -Suite CI
|
||||
```
|
||||
|
||||
- `Unit`:构建核心静态库并运行 VS Native Unit Test。
|
||||
- `NonUI`:运行 Debug x64、Debug x64 ASan、Release x64、Debug Win32 和 KEY1 至 KEY6 编译,不启动或操作任何窗口。
|
||||
- `Render`:运行确定性 EasyX fixture 和关键像素断言;当前与单轮 UI fixture 共用实现。
|
||||
- `UI`:使用 `SendInput` 驱动 ownership、geometry、tooltip、tab、table、dialog 六类真实窗口场景。
|
||||
- `All`:Debug x64、Debug x64 ASan、Release x64、Debug Win32、KEY1 至 KEY6 编译、UI 连续三轮。
|
||||
- `CI`:默认运行 Debug x64、Release x64 和 Debug Win32;设置 `STELLARX_UI_TESTS=1` 后增加单轮 UI。
|
||||
|
||||
Windows MSVC AddressSanitizer 不支持 LeakSanitizer,因此脚本使用 `detect_leaks=0:halt_on_error=1`。ASan 产物独立写入 `build\x64\Debug-asan`,不会污染普通 Debug 构建。
|
||||
|
||||
`scripts/test.ps1` 使用 UTF-8 BOM 保存,确保 Windows PowerShell 5.1 能正确读取中文注释,不会把注释后的语句误判为注释内容。
|
||||
|
||||
## 覆盖范围
|
||||
|
||||
逻辑测试覆盖:
|
||||
|
||||
- 稳定句柄与回调期间 self/sibling/add/remove/clear 所有权变更。
|
||||
- Tab 延迟新增、失败返还所有权、重复标题和页面激活状态。
|
||||
- 字体名拥有权与内容比较。
|
||||
- MBCS 密码掩码、非法尾字节容错。
|
||||
- Tooltip 单调时钟 tick、禁用和移出清理。
|
||||
- 1000 条消息的 128 条批处理上限、空闲休眠和关闭顺序。
|
||||
- 日志配置原子切换与滚动行完整性。
|
||||
|
||||
UI/像素测试覆盖:
|
||||
|
||||
- 回调删除自身后旧区域恢复。
|
||||
- Button、Label、TextBox、Canvas 的移动、缩放和隐藏残影。
|
||||
- 静止 Tooltip 到期显示及移出清除。
|
||||
- 同名页签唯一选中、唯一页面可见。
|
||||
- 极窄 Table 的最小宽度和 coverage 外像素不变量。
|
||||
- 两层异步 Dialog 先后关闭、下层可见且可继续交互。
|
||||
|
||||
## 诊断产物
|
||||
|
||||
所有产物位于未跟踪的 `TestResults\`:
|
||||
|
||||
- 单元测试:TRX。
|
||||
- UI 成功:场景 JSON、`screen.png`、fixture 标准输出和错误日志。
|
||||
- UI 失败:`driver-error.json`、`failure.png`,以及已生成的场景中间截图。
|
||||
|
||||
UI fixture 按 PID 和精确标题枚举 `EasyXWnd`,使用 `PrintWindow(PW_CLIENTONLY)` 获取不受遮挡影响的客户区像素,不使用 DPI 和字体敏感的全图 golden image。
|
||||
|
||||
## 人工 UI 验收
|
||||
|
||||
本轮收口不自动启动 UI。发布或推送前,由人工依次验证 KEY1、KEY2、KEY4、KEY5、KEY6;KEY3 只做启动兼容性冒烟。
|
||||
|
||||
### 构建与启动
|
||||
|
||||
推荐双击仓库根目录的 `Run-Key-Test.cmd`,输入 `1` 至 `6` 后,脚本会重新构建并启动对应场景。关闭测试窗口后会返回选择菜单,输入 `0` 退出;脚本只负责构建和启动,不会自动操作 UI。
|
||||
|
||||
也可以从 PowerShell 直接启动指定场景:
|
||||
|
||||
```powershell
|
||||
.\scripts\run-key.ps1 -Key 5
|
||||
```
|
||||
|
||||
若启动器不可用,将下面命令中的 `KeyScenario` 改为待测编号。每次构建完成后手动启动生成的程序:
|
||||
|
||||
```powershell
|
||||
$msbuild = 'D:\vs2022\MSBuild\Current\Bin\MSBuild.exe'
|
||||
& $msbuild .\imGui-easyX.vcxproj /m /t:Rebuild /p:Configuration=Debug /p:Platform=x64 /p:KeyScenario=1 /p:EnableAsan=false /v:minimal
|
||||
.\build\x64\Debug\imGui-easyX.exe
|
||||
```
|
||||
|
||||
### 通用通过标准
|
||||
|
||||
- 窗口可正常启动、缩放、最大化、还原和关闭,无崩溃、卡死或断言。
|
||||
- Hover、按下、释放和 Tooltip 状态及时变化,鼠标移出后不残留高亮、文字或浮层。
|
||||
- 切页、分页、移动或缩放后无旧像素、黑块、重影、错层和控件穿透。
|
||||
- 一次点击只触发一次结果;禁用控件不触发回调;模态对话框阻止底层交互。
|
||||
|
||||
### 必测场景
|
||||
|
||||
| 场景 | 操作 | 通过标准 |
|
||||
|---|---|---|
|
||||
| KEY1 | 进入页签 1,点击页内“按钮”重置表格,不移动鼠标立即观察分页区;连续重置三次并执行上一页、下一页;点击外部 `test` 多次激活同一页,再依次切换三个页签 | 重置后的首帧立即完整显示“第1页/共2页”,不依赖 hover 或后续重绘恢复;重复激活不闪烁;Table 超出页面区域的内容在切页后完全清除;无残影或层级错乱 |
|
||||
| KEY2 | 拉宽、缩窄、最大化并还原窗口;点击多个位按钮,执行位取反、左移、右移、一键置 0/1 和有符号切换 | 32 位“位号 + 按钮”始终对齐;五个区块铺满且不重叠;十六进制、十进制、上次值、本次值立即同步 |
|
||||
| KEY4 | 打开非模态对话框,在顶部按钮、页签和分页区快速移动鼠标;再打开模态对话框并拖动窗口,随后关闭 | 遮挡区不发生 Hover/Tooltip 穿透;标题、关闭按钮和底层背景无残影;关闭后底层 Hover 立即恢复 |
|
||||
| KEY5 | 在三层 Canvas、两个页签及其页内按钮间移动和点击;触发各处 Tooltip;操作 Table 分页按钮并观察粉色浮层 | 深层按钮状态正常;Tooltip 出现和清除完整;页签与页面层级正确;Table 重绘不会擦除或覆盖上层浮层 |
|
||||
| KEY6 | 点击圆形和椭圆按钮的内部及外接矩形角落;点击禁用按钮;切换右侧页签并操作页内控件;分别验证 YesNo、RetryCancel、AbortRetryIgnore | 形状外区域不误触发;禁用按钮不改变状态;右侧页签布局稳定;消息框结果正确,非模态关闭后无残留且底层 Hover 恢复 |
|
||||
| KEY3 | 启动后完成登录页和旧业务页的基本切换 | 能正常进入和切换,无明显布局或交互倒退 |
|
||||
|
||||
### 本轮验收记录
|
||||
|
||||
- 验收日期:2026-09-30
|
||||
- 验收方式:用户按上述清单手工执行 KEY1 至 KEY6。
|
||||
- 验收结论:全部场景已完成,当前快照暂未发现明显问题。
|
||||
- 自动复验:2026-09-30 使用 Windows PowerShell 5.1 执行 `NonUI`,四种配置均为 15/15 通过,KEY1 至 KEY6 编译链接成功。
|
||||
- 说明:该结论代表本轮人工回归通过,不等同于对所有运行环境和未覆盖路径作无缺陷保证。
|
||||
|
||||
### 失败留证
|
||||
|
||||
- 记录 KEY 编号、窗口尺寸、操作顺序和实际现象。
|
||||
- 截取包含整个客户区的截图;闪烁或延迟问题录制短视频。
|
||||
- 若崩溃,保留 Visual Studio 调用栈和当次日志,不继续把该快照标记为 UI 通过。
|
||||
@@ -0,0 +1,70 @@
|
||||
# 功能变更 ID: Feature-20251220-0001
|
||||
|
||||
> 适用场景:记录小到中等规模的功能、接口、行为、默认值或配置变化。
|
||||
> 不适用场景:新增核心模块、重大模块重构、架构级设计,请使用“新增功能模块”模板。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: Feature-20251220-0001
|
||||
- 标题: Table 运行期重置表头与数据 API
|
||||
- 状态:已完成(历史记录;本次仅归档,未重新验证)
|
||||
- 类型:新增
|
||||
- 级别:L2 中等
|
||||
- 模块: Table
|
||||
- 版本 / 分支: v2.3.2 / master
|
||||
- 环境: Windows / Visual Studio 2022 / Debug、Release / EasyX
|
||||
- 负责人: Ysm-04(历史实现)
|
||||
|
||||
## 变更背景
|
||||
|
||||
- 背景: 原有 Table 可以设置表头和数据,但缺少明确的运行期清空与整体重置入口。
|
||||
- 目标: 允许同一个 Table 实例在运行过程中切换不同的数据集合,并在重置后重新计算单元格尺寸、分页信息和绘制状态。
|
||||
- 不做什么:[可选]
|
||||
- 不改变原有 `setHeaders()`、`setData()` 的调用方式。
|
||||
- 不引入新的表格数据模型或外部依赖。
|
||||
|
||||
## 变更内容
|
||||
|
||||
- 变更摘要: 新增清空表头、清空数据以及同时清空二者的公共 API。
|
||||
- 新增项:[可选]
|
||||
- `void Table::clearHeaders()`
|
||||
- `void Table::clearData()`
|
||||
- `void Table::resetTable()`
|
||||
- 修改项:[可选]
|
||||
- 清空表头后,重新标记单元格尺寸、表头以及分页按钮/页码信息需要计算或绘制。
|
||||
- 清空数据后,将 `currentPage`、`totalPages` 重置为 1,并重新标记尺寸和分页信息。
|
||||
- `resetTable()` 依次复用 `clearHeaders()` 与 `clearData()`。
|
||||
- 删除 / 废弃项:[可选]
|
||||
- 无。
|
||||
- 受影响的文件 / 类 / 函数:
|
||||
- 历史路径:`include/StellarX/table.h`
|
||||
- 历史路径:`src/table.cpp`
|
||||
- `Table::clearHeaders()`、`Table::clearData()`、`Table::resetTable()`
|
||||
- 对外 API / 属性变化:[可选]
|
||||
- 新增上述三个公共方法;旧 API 保持不变。
|
||||
|
||||
## 行为对照
|
||||
|
||||
- 变更前: Table 创建后缺少专用的清空接口,调用方难以明确地复用同一对象展示另一组表头和数据。
|
||||
- 变更后:
|
||||
- `clearHeaders()` 清空表头并触发后续尺寸、表头和分页布局更新。
|
||||
- `clearData()` 清空数据、复位页码状态并触发后续重算。
|
||||
- `resetTable()` 同时清空表头与数据,调用方随后可重新设置内容。
|
||||
- 兼容性说明:兼容
|
||||
- 迁移说明:[可选]
|
||||
- 无需迁移旧代码;需要复用 Table 时可在重新调用 `setHeaders()` / `setData()` 前使用新增接口。
|
||||
|
||||
## 验证与落地
|
||||
|
||||
- 验证方式:
|
||||
- 历史记录使用按钮回调分别执行“清空并重设表头”“清空并重设数据”“同时清空并重设表头与数据”。
|
||||
- 观察表头、数据、表格尺寸和分页区域是否同步更新。
|
||||
- 验证结果:
|
||||
- PDF 历史记录将上述三个手工场景记为通过。
|
||||
- 本次归档未构建 v2.3.2,也未重新运行 GUI 用例;历史运行结果未在本次复验。
|
||||
- 关联 BUG / Fix:[可选]
|
||||
- Commit: `0c1cf2938f0d6c5684eeb318f17a8443e0c8cf5a`
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选] v2.3.2
|
||||
- 备注:[可选]
|
||||
- 资料来源:`StellarX功能_模块变更.pdf` 第 19-21 页,并以对应提交和历史 `CHANGELOG.md` 交叉核对。
|
||||
@@ -0,0 +1,66 @@
|
||||
# 功能变更 ID: Feature-20251220-0002
|
||||
|
||||
> 适用场景:记录小到中等规模的功能、接口、行为、默认值或配置变化。
|
||||
> 不适用场景:新增核心模块、重大模块重构、架构级设计,请使用“新增功能模块”模板。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: Feature-20251220-0002
|
||||
- 标题: TextBox 密码模式
|
||||
- 状态:已完成(历史记录;本次仅归档,未重新验证)
|
||||
- 类型:新增
|
||||
- 级别:L2 中等
|
||||
- 模块: TextBox / CoreTypes
|
||||
- 版本 / 分支: v2.3.2 / master
|
||||
- 环境: Windows / Visual Studio 2022 / Debug、Release / EasyX
|
||||
- 负责人: Ysm-04(历史实现)
|
||||
|
||||
## 变更背景
|
||||
|
||||
- 背景: TextBox 原有输入模式会直接绘制用户输入,不适合密码等需要隐藏显示内容的场景。
|
||||
- 目标: 保存真实输入值,但在界面上使用掩码字符显示,并允许调用方通过 `getText()` 获取原始文本。
|
||||
- 不做什么:[可选]
|
||||
- 不提供密码加密、散列、数据库校验或安全存储能力。
|
||||
- 不改变既有输入模式的行为。
|
||||
|
||||
## 变更内容
|
||||
|
||||
- 变更摘要: 为 `TextBoxmode` 增加 `PASSWORD_MODE`,并在 TextBox 输入和绘制路径中处理该模式。
|
||||
- 新增项:[可选]
|
||||
- `StellarX::TextBoxmode::PASSWORD_MODE`
|
||||
- 修改项:[可选]
|
||||
- `TextBox::draw()` 在密码模式下构造掩码文本并绘制掩码,不直接绘制真实内容。
|
||||
- `TextBox::handleEvent()` 在密码模式下获取输入并保存真实文本。
|
||||
- 删除 / 废弃项:[可选]
|
||||
- 无。
|
||||
- 受影响的文件 / 类 / 函数:
|
||||
- 历史路径:`include/StellarX/CoreTypes.h`
|
||||
- 历史路径:`src/textBox.cpp`
|
||||
- `TextBoxmode`、`TextBox::draw()`、`TextBox::handleEvent()`、`TextBox::getText()`
|
||||
- 对外 API / 属性变化:[可选]
|
||||
- `TextBoxmode` 新增枚举值 `PASSWORD_MODE`。
|
||||
|
||||
## 行为对照
|
||||
|
||||
- 变更前: TextBox 只能按普通输入或只读模式展示文本,输入内容会直接显示。
|
||||
- 变更后: 设置为 `PASSWORD_MODE` 后,真实内容仍保存在 TextBox 中,绘制层使用 `*` 作为掩码,`getText()` 返回真实内容。
|
||||
- 兼容性说明:兼容
|
||||
- 迁移说明:[可选]
|
||||
- 原有模式无需迁移;密码输入场景显式选择 `PASSWORD_MODE`。
|
||||
- 此功能只隐藏界面显示,不应被视为密码安全存储方案。
|
||||
|
||||
## 验证与落地
|
||||
|
||||
- 验证方式:
|
||||
- 创建密码模式 TextBox,输入内容,确认界面显示掩码且 `getText()` 返回原始文本。
|
||||
- 将 `getText()` 结果交给业务层进行密码比对。
|
||||
- 验证结果:
|
||||
- PDF 历史记录将普通密码输入和业务比对两个手工场景记为通过。
|
||||
- 历史实现按 `std::string::size()` 生成掩码,即按字节而非明确按多字节逻辑字符计数;中文等 MBCS 输入的掩码数量不能据此认定为已正确验证。
|
||||
- 本次归档未构建 v2.3.2,也未重新运行 GUI 用例。
|
||||
- 关联 BUG / Fix:[可选]
|
||||
- Commit: `0c1cf2938f0d6c5684eeb318f17a8443e0c8cf5a`
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选] v2.3.2
|
||||
- 备注:[可选]
|
||||
- 资料来源:`StellarX功能_模块变更.pdf` 第 22-23 页,并以对应提交和历史 `CHANGELOG.md` 交叉核对。
|
||||
@@ -0,0 +1,67 @@
|
||||
# 功能变更 ID: Feature-20260109-0001
|
||||
|
||||
> 适用场景:记录小到中等规模的功能、接口、行为、默认值或配置变化。
|
||||
> 不适用场景:新增核心模块、重大模块重构、架构级设计,请使用“新增功能模块”模板。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: Feature-20260109-0001
|
||||
- 标题: TabControl 页签切换时序调整
|
||||
- 状态:已完成(历史记录;本次仅归档,未重新验证)
|
||||
- 类型:修改
|
||||
- 级别:L2 中等
|
||||
- 模块: TabControl
|
||||
- 版本 / 分支: v3.0.0 / master
|
||||
- 环境: Windows 11 / Visual Studio 2022 / Debug、Release / EasyX
|
||||
- 负责人: Ysm-04(历史实现)
|
||||
|
||||
## 变更背景
|
||||
|
||||
- 背景: 原页签切换回调先显示目标页,再关闭其他页,在复杂容器和背景快照链路中可能短暂形成多页同时可见的状态。
|
||||
- 目标: 将切换顺序收敛为先停用原页,再激活目标页,降低重绘和快照时序的不确定性。
|
||||
- 不做什么:[可选]
|
||||
- 不改变 TabControl 的公共调用接口。
|
||||
- 不改变页签按钮与页面 Canvas 的配对模型。
|
||||
|
||||
## 变更内容
|
||||
|
||||
- 变更摘要: 调整 `TabControl::add()` 注册的 Toggle 回调执行顺序。
|
||||
- 新增项:[可选]
|
||||
- 激活日志记录原页索引、目标页索引和页签文本。
|
||||
- 修改项:[可选]
|
||||
- 激活目标页前,先找到当前可见页面并停用其他已点击页签。
|
||||
- 其他页签的 ToggleOff 回调负责隐藏对应页面。
|
||||
- 旧页停用后,再更新目标页布局、显示目标页并标记 TabControl 为脏。
|
||||
- 删除 / 废弃项:[可选]
|
||||
- 无。
|
||||
- 受影响的文件 / 类 / 函数:
|
||||
- 历史路径:`src/TabControl.cpp`
|
||||
- `TabControl::add()` 内的 ToggleOn / ToggleOff 回调
|
||||
- 对外 API / 属性变化:[可选]
|
||||
- 无,属于内部切换时序调整。
|
||||
|
||||
## 行为对照
|
||||
|
||||
- 变更前: 先显示目标页面,再遍历关闭其他页签和页面。
|
||||
- 变更后: 先通过其他页签的关闭回调隐藏旧页面,再显示并激活目标页面。
|
||||
- 兼容性说明:兼容
|
||||
- 迁移说明:[可选]
|
||||
- 调用方无需改动;若业务代码依赖切换过程中两个页面短暂同时可见的副作用,应移除该依赖。
|
||||
|
||||
## 验证与落地
|
||||
|
||||
- 验证方式:
|
||||
- 分别创建包含 3 个和 5 个页签的 TabControl,连续点击不同页签。
|
||||
- 检查日志顺序,确认旧页先关闭、目标页后激活。
|
||||
- 重复触发同一页签,观察激活和关闭行为。
|
||||
- 验证结果:
|
||||
- PDF 历史记录将 3 页签、5 页签和同页签重复触发场景记为正常。
|
||||
- 本次归档未重新构建 v3.0.0,也未运行 GUI 回归,因此不将历史手工观察视为当前版本验证结果。
|
||||
- 关联 BUG / Fix:[可选]
|
||||
- 与页签切换后的绘制一致性相关:[`BUG-20260109-0002-TabControl切换后Table新旧内容重叠.md`](../BUG/BUG-20260109-0002-TabControl切换后Table新旧内容重叠.md)
|
||||
- 后续修复记录:[`Fix-BUG-20260109-0002-TabControl切换后Table新旧内容重叠.md`](../Fix/Fix-BUG-20260109-0002-TabControl切换后Table新旧内容重叠.md)
|
||||
- Commit: `5cb59b36525cde93de83313551296c6458694110`
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选] v3.0.0
|
||||
- 备注:[可选]
|
||||
- 资料来源:`StellarX功能_模块变更.pdf` 第 13-15 页,并以对应提交和历史 `CHANGELOG.md` 交叉核对。
|
||||
@@ -0,0 +1,66 @@
|
||||
# 功能变更 ID: Feature-20260109-0002
|
||||
|
||||
> 适用场景:记录小到中等规模的功能、接口、行为、默认值或配置变化。
|
||||
> 不适用场景:新增核心模块、重大模块重构、架构级设计,请使用“新增功能模块”模板。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: Feature-20260109-0002
|
||||
- 标题: 删除 Button 尺寸别名 API
|
||||
- 状态:已完成(历史记录;本次仅归档,未重新验证)
|
||||
- 类型:删除
|
||||
- 级别:L2 中等
|
||||
- 模块: Button / Control
|
||||
- 版本 / 分支: v3.0.0 / master
|
||||
- 环境: Windows 11 / Visual Studio 2022 / Debug、Release / EasyX
|
||||
- 负责人: Ysm-04(历史实现)
|
||||
|
||||
## 变更背景
|
||||
|
||||
- 背景: Button 自身提供的 `getButtonWidth()` / `getButtonHeight()` 与基类 `Control::getWidth()` / `getHeight()` 的语义和返回值重复。
|
||||
- 目标: 同一尺寸语义只保留基类正式入口,减少 API 冗余和派生类维护成本。
|
||||
- 不做什么:[可选]
|
||||
- 不改变 Button 的宽高字段、尺寸计算或绘制行为。
|
||||
- 不提供旧名称的兼容包装函数。
|
||||
|
||||
## 变更内容
|
||||
|
||||
- 变更摘要: 删除 Button 层重复的尺寸获取方法,统一使用 Control 基类接口。
|
||||
- 新增项:[可选]
|
||||
- 无。
|
||||
- 修改项:[可选]
|
||||
- Button 宽高获取入口统一为 `getWidth()` / `getHeight()`。
|
||||
- 删除 / 废弃项:[可选]
|
||||
- `int Button::getButtonWidth() const`
|
||||
- `int Button::getButtonHeight() const`
|
||||
- 受影响的文件 / 类 / 函数:
|
||||
- 历史路径:`include/StellarX/Button.h`
|
||||
- `Button`、`Control::getWidth()`、`Control::getHeight()`
|
||||
- 对外 API / 属性变化:[可选]
|
||||
- 删除两个 Button 公共方法,属于源码级破坏性变更。
|
||||
|
||||
## 行为对照
|
||||
|
||||
- 变更前: 调用方既可通过 Button 专用别名,也可通过继承自 Control 的接口获取相同尺寸。
|
||||
- 变更后: 只保留 `getWidth()` 和 `getHeight()`;读取的仍是同一组 `width` / `height` 状态。
|
||||
- 兼容性说明:不兼容
|
||||
- 迁移说明:[可选]
|
||||
- `getButtonWidth()` -> `getWidth()`
|
||||
- `getButtonHeight()` -> `getHeight()`
|
||||
- 源码调用必须替换;若使用预编译库,升级后应重新编译和链接。
|
||||
|
||||
## 验证与落地
|
||||
|
||||
- 验证方式:
|
||||
- 创建 Button 并设置尺寸,检查 `getWidth()` / `getHeight()` 返回值。
|
||||
- 全量搜索并编译调用点,确认不存在已删除 API 的残留引用。
|
||||
- 运行 Button 示例,观察布局、命中区域和重绘行为。
|
||||
- 验证结果:
|
||||
- PDF 记录了预期验证方式,但没有附带构建日志、测试报告或截图。
|
||||
- 本次归档确认历史提交和 v3.0.0 更新日志包含该删除项,但没有重新构建旧版本。
|
||||
- 关联 BUG / Fix:[可选]
|
||||
- Commit: `5cb59b36525cde93de83313551296c6458694110`
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选] v3.0.0
|
||||
- 备注:[可选]
|
||||
- 资料来源:`StellarX功能_模块变更.pdf` 第 16-18 页,并以对应提交和历史 `CHANGELOG.md` 交叉核对。
|
||||
@@ -0,0 +1,71 @@
|
||||
# 功能变更 ID: Feature-20260317-0001
|
||||
|
||||
> 适用场景:记录小到中等规模的功能、接口、行为、默认值或配置变化。
|
||||
> 不适用场景:新增核心模块、重大模块重构、架构级设计,请使用“新增功能模块”模板。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: Feature-20260317-0001
|
||||
- 标题: TextBox 文本溢出截断
|
||||
- 状态:已完成(历史记录;本次仅归档,未重新验证)
|
||||
- 类型:修改
|
||||
- 级别:L2 中等
|
||||
- 模块: TextBox
|
||||
- 版本 / 分支: v3.0.1 / master
|
||||
- 环境: Windows / Visual Studio 2022 / Debug、Release / EasyX
|
||||
- 负责人: Ysm-04(历史实现)
|
||||
|
||||
## 变更背景
|
||||
|
||||
- 背景: TextBox 原先只限制存储字符数,文本像素宽度仍可能超过控件可绘制区域。
|
||||
- 目标: 绘制时根据实际像素宽度生成带省略号的显示文本,避免文字覆盖相邻控件,同时保持内部原文不变。
|
||||
- 不做什么:[可选]
|
||||
- 不修改 `getText()` / `setText()` 的签名。
|
||||
- 不截断或覆盖 TextBox 内部保存的原始文本。
|
||||
- 不新增持久缓存或第三方依赖。
|
||||
|
||||
## 变更内容
|
||||
|
||||
- 变更摘要: `TextBox::draw()` 将存储文本或密码掩码复制到临时 `displayText`,超出可用宽度时逐步缩短并追加 `...`。
|
||||
- 新增项:[可选]
|
||||
- 绘制期临时变量 `displayText`。
|
||||
- 绘制期截断状态 `isTextTruncated`(历史实现中仅作为局部标记,未形成公共状态)。
|
||||
- 修改项:[可选]
|
||||
- 使用 `width - 20` 作为保留左右边距后的可用宽度。
|
||||
- 普通模式和密码模式统一通过 `displayText` 计算尺寸并绘制。
|
||||
- 删除 / 废弃项:[可选]
|
||||
- 无。
|
||||
- 受影响的文件 / 类 / 函数:
|
||||
- 历史路径:`src/textBox.cpp`
|
||||
- `TextBox::draw()`
|
||||
- 对外 API / 属性变化:[可选]
|
||||
- 无;显示行为增强,`getText()` 仍返回完整原文。
|
||||
|
||||
## 行为对照
|
||||
|
||||
- 变更前: 文本字符数未超过 `maxCharLen` 时仍可能因字体或内容宽度过大而绘制到 TextBox 边界之外。
|
||||
- 变更后: 正常宽度条件下,绘制文本会缩短到可用区域并追加省略号,内部原文不受影响。
|
||||
- 兼容性说明:兼容
|
||||
- 迁移说明:[可选]
|
||||
- 无需迁移;调用方如需完整内容,继续使用 `getText()`。
|
||||
|
||||
## 验证与落地
|
||||
|
||||
- 验证方式:
|
||||
- 普通文本、空文本、长文本、大字号和密码模式下检查显示边界。
|
||||
- 调整控件宽度,检查截断与恢复后的显示。
|
||||
- 对照 `getText()`,确认内部原文没有被截断。
|
||||
- 验证结果:
|
||||
- PDF 描述了目标、算法和测试方向,但“验证证据”仍是待填占位内容,没有附带截图、日志或自动化报告。
|
||||
- 历史源码确认普通路径采用逐字节 `pop_back()` 并重复调用 `textwidth()`;因此不能仅凭原记录认定中文等多字节文本一定不会被截在半个字符上。
|
||||
- 当 `availableWidth <= 0` 时,历史条件不会进入截断分支,极小控件宽度也不能认定为已经正确覆盖。
|
||||
- 循环最多删除 n 个字节,但每轮重新测量字符串宽度;若宽度计算与字符串长度相关,最坏时间成本可能接近 O(n²),而不是原记录笼统声称的 O(n)。
|
||||
- 本次归档未重新构建 v3.0.1,也未运行 GUI 用例。
|
||||
- 关联 BUG / Fix:[可选]
|
||||
- Commit:
|
||||
- 实现提交:`c66cad3423a46ec4ef902965baa4c1816d10148a`
|
||||
- 发布记录提交:`b765e54eff28ebcd0bd99a81dec80b3ac0769337`
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选] v3.0.1
|
||||
- 备注:[可选]
|
||||
- 资料来源:`StellarX功能_模块变更.pdf` 第 3-9 页,并以对应提交和历史 `CHANGELOG.md` 交叉核对。
|
||||
@@ -0,0 +1,77 @@
|
||||
# 功能变更 ID: Feature-20260317-0002
|
||||
|
||||
> 适用场景:记录小到中等规模的功能、接口、行为、默认值或配置变化。
|
||||
> 不适用场景:新增核心模块、重大模块重构、架构级设计,请使用“新增功能模块”模板。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: Feature-20260317-0002
|
||||
- 标题: TextBox::setText 运行期刷新语义调整
|
||||
- 状态:已完成(历史记录;本次仅归档,未重新验证)
|
||||
- 类型:修改
|
||||
- 级别:L2 中等
|
||||
- 模块: TextBox / Repaint
|
||||
- 版本 / 分支: v3.0.1 / master
|
||||
- 环境: Windows 11 / Visual Studio 2022 / Debug、Release / EasyX
|
||||
- 负责人: Ysm-04(历史实现)
|
||||
|
||||
## 变更背景
|
||||
|
||||
- 背景:
|
||||
- 早期 `setText()` 直接调用 `draw()`,在图形上下文尚未初始化时可能崩溃。
|
||||
- v3.0.0 将 Setter 收敛为更新状态并标脏,解决初始化阶段的生命周期问题,但运行期程序化设置文本可能要等后续事件或重绘周期才能显示。
|
||||
- 目标: 在不破坏初始化阶段安全性的前提下,让已经绘制过的 TextBox 在运行期更新文本后请求或执行刷新。
|
||||
- 不做什么:[可选]
|
||||
- 不改变 `setText(std::string)` 的函数签名。
|
||||
- 不在没有有效背景快照时强制进入绘制链路。
|
||||
|
||||
## 变更内容
|
||||
|
||||
- 变更摘要: `setText()` 根据文本是否变化、最大长度、父控件和有效快照状态决定仅标脏、向父控件请求重绘或直接绘制。
|
||||
- 新增项:[可选]
|
||||
- 文本未变化时立即返回,避免无效更新。
|
||||
- 修改项:[可选]
|
||||
- 文本变化后始终更新内部值并标记为脏。
|
||||
- 有父控件且 `hasSnap` 为真时,调用 `requestRepaint(parent)`。
|
||||
- 无父控件且 `hasSnap` 为真时,直接调用 `draw()`。
|
||||
- `hasSnap` 为假时只保留脏标记,等待首次绘制或统一重绘流程。
|
||||
- 删除 / 废弃项:[可选]
|
||||
- 无。
|
||||
- 受影响的文件 / 类 / 函数:
|
||||
- 历史路径:`src/textBox.cpp`
|
||||
- `TextBox::setText(std::string)`
|
||||
- 对外 API / 属性变化:[可选]
|
||||
- 函数签名不变;运行期可见刷新时机发生变化。
|
||||
|
||||
## 行为对照
|
||||
|
||||
- 变更前(v3.0.0): `setText()` 更新内部文本并标脏,不保证调用后立即触发可见重绘。
|
||||
- 变更后(v3.0.1):
|
||||
- 初始化/首次绘制前没有有效快照,仅更新文本并标脏,不触发依赖图形上下文的绘制。
|
||||
- 已绘制且存在有效快照时,通过父容器请求重绘;没有父控件时直接绘制。
|
||||
- 文本未变化时不执行重复重绘。
|
||||
- 兼容性说明:部分兼容
|
||||
- 迁移说明:[可选]
|
||||
- 若旧代码在 `setText()` 后手动调用 `draw()`,升级到 v3.0.1 后应移除重复绘制,避免闪烁或重复刷新。
|
||||
- 初始化阶段仍可先设置默认文本,由首次统一绘制完成显示。
|
||||
|
||||
## 验证与落地
|
||||
|
||||
- 验证方式:
|
||||
- 创建 TextBox,在窗口和图形上下文建立前调用 `setText()`,确认不会进入绘制链路或中断。
|
||||
- 窗口运行后,在按钮回调中调用 `setText()`,确认无需额外鼠标事件即可刷新。
|
||||
- 重复设置相同文本,确认不会产生无效刷新。
|
||||
- 验证结果:
|
||||
- PDF 历史记录将“初始化前设置默认文本”和“按钮回调中运行期更新”记为正常。
|
||||
- 本次归档通过历史提交确认了 `hasSnap` 分支逻辑,但未构建 v3.0.1,也未重新运行 GUI 用例。
|
||||
- `hasSnap` 是历史实现用于判断是否已具备绘制条件的代理状态;本记录不把它扩展解释为所有图形上下文状态的通用保证。
|
||||
- 关联 BUG / Fix:[可选]
|
||||
- 前序问题:[`BUG-20260109-0001-TextBox-setText进入事件循环前调用崩溃.md`](../BUG/BUG-20260109-0001-TextBox-setText进入事件循环前调用崩溃.md)
|
||||
- v3.0.0 修复:[`Fix-BUG-20260109-0001-TextBox-setText进入事件循环前调用崩溃.md`](../Fix/Fix-BUG-20260109-0001-TextBox-setText进入事件循环前调用崩溃.md)
|
||||
- Commit:
|
||||
- 实现提交:`c66cad3423a46ec4ef902965baa4c1816d10148a`
|
||||
- 发布记录提交:`b765e54eff28ebcd0bd99a81dec80b3ac0769337`
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选] v3.0.1
|
||||
- 备注:[可选]
|
||||
- 资料来源:`StellarX功能_模块变更.pdf` 第 10-12 页,并以对应提交和历史 `CHANGELOG.md` 交叉核对。
|
||||
@@ -44,7 +44,7 @@
|
||||
- 删除 / 废弃项:[可选]
|
||||
- 删除过长、重复的说明文案。
|
||||
- 受影响的文件 / 类 / 函数:
|
||||
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp)
|
||||
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/app/z-testDome.cpp)
|
||||
- 对外 API / 属性变化:[可选]
|
||||
- 无
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
- 废弃旧的“位标签和位按钮分别做比例位移”的选择区组织方式
|
||||
- 废弃散在多个回调中的局部显示刷新
|
||||
- 受影响的文件 / 类 / 函数:
|
||||
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp)
|
||||
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/app/z-testDome.cpp)
|
||||
- 对外 API / 属性变化:[可选]
|
||||
- 无框架级 API 变化,属于测试用例迁移
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
- 删除 / 废弃项:[可选]
|
||||
- 无
|
||||
- 受影响的文件 / 类 / 函数:
|
||||
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp)
|
||||
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/app/z-testDome.cpp)
|
||||
- 对外 API / 属性变化:[可选]
|
||||
- 无
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
## 验证与落地
|
||||
|
||||
- 验证方式:
|
||||
- 对照当前 [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp) 中 `KEY1 ~ KEY5` 的实际结构与近期修复范围进行人工核对。
|
||||
- 对照当前 [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/app/z-testDome.cpp) 中 `KEY1 ~ KEY5` 的实际结构与近期修复范围进行人工核对。
|
||||
- 验证结果:
|
||||
- 当前矩阵与现有测试入口和阶段主线一致。
|
||||
- 关联 BUG / Fix:[可选]
|
||||
|
||||
@@ -47,9 +47,9 @@
|
||||
- 删除 / 废弃项:[可选]
|
||||
- 无
|
||||
- 受影响的文件 / 类 / 函数:
|
||||
- [`Control.h`](D:/programming/imGUI-easyX/imGui-easyX/Control.h)
|
||||
- [`Control.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Control.cpp)
|
||||
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp)
|
||||
- [`Control.h`](D:/programming/imGUI-easyX/imGui-easyX/include/StellarX/Control.h)
|
||||
- [`Control.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Control.cpp)
|
||||
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/app/z-testDome.cpp)
|
||||
- 对外 API / 属性变化:[可选]
|
||||
- `setHorizontalLayoutSpec(...)`
|
||||
- `setVerticalLayoutSpec(...)`
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
# 功能变更 ID: Feature-20260915-0001
|
||||
|
||||
> 适用场景:记录小到中等规模的功能、接口、行为、默认值或配置变化。
|
||||
> 不适用场景:新增核心模块、重大模块重构、架构级设计,请使用“新增功能模块”模板。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: Feature-20260915-0001
|
||||
- 标题: 全量缺陷修复与 API 调整
|
||||
- 状态:非 UI 与人工 UI 已验证,待提交
|
||||
- 类型:修改 / 删除 / 重命名
|
||||
- 级别:L3 重大
|
||||
- 模块: Control / Window / Canvas / Dialog / TabControl / Table / SxLogger / Test
|
||||
- 版本 / 分支: 当前工作区
|
||||
- 环境: Windows + Visual Studio 2022 + MSVC v143 + EasyX
|
||||
- 负责人: Codex 协作修改
|
||||
|
||||
## 变更背景
|
||||
|
||||
- 背景:
|
||||
- 代码审查发现控件回调期间所有权变化、空 TabControl、快照尺寸、几何残影、Dialog 关闭、Table coverage、日志滚动、MBCS 文本、Tooltip 和事件循环等问题。
|
||||
- 原有测试主要依赖 `KEY` 场景和手工观察,缺少不创建窗口的逻辑验证、ASan 验证以及可重复的外部 UI 断言。
|
||||
- 目标:
|
||||
- 修复高、中、低严重度缺陷,消除明确的 UAF、越界、残影、数据截断和所有权丢失风险。
|
||||
- 通过稳定句柄和延迟变更事务收口控件容器生命周期。
|
||||
- 建立可重复的逻辑、像素不变量和真实 UI 回归验证。
|
||||
- 明确破坏性 API 的迁移方式。
|
||||
- 不做什么:[可选]
|
||||
- 不迁移 Unicode,继续使用 MBCS/GBK 风格字符处理。
|
||||
- 不把 GUI 改造成多线程模型。
|
||||
- 不在本次将 `StellarX.Core` 改为 DLL,也不处理安装包制作。
|
||||
|
||||
## 变更内容
|
||||
|
||||
- 变更摘要:
|
||||
- 修复 14 项高、中、低严重度问题。
|
||||
- 将核心框架独立为 `StellarX.Core` 静态库,并让应用、单元测试和 UI Fixture 共用该实现。
|
||||
- 新增 Native Unit Test、ASan、EasyX UI Fixture 和 PowerShell 自动化入口。
|
||||
- 新增项:[可选]
|
||||
- `StellarX::ControlHandle` 及受控控件所有权接口。
|
||||
- `MutationTransaction` 延迟变更机制。
|
||||
- `SxEventLoopInternal.h` 和 `SxTextInternal.h` 内部测试辅助。
|
||||
- `StellarX.Tests`、`StellarX.UiFixture` 和 `scripts/test.ps1`。
|
||||
- 修改项:[可选]
|
||||
- Button/Label/TextBox/Canvas/Table/TabControl 的 geometry、snapshot、coverage 和绘制提交逻辑。
|
||||
- Dialog 关闭和 overlay 收口逻辑。
|
||||
- SxLogger sink 配置和滚动逻辑。
|
||||
- Tooltip tick、MBCS 密码掩码和事件循环批处理。
|
||||
- 删除 / 废弃项:[可选]
|
||||
- 删除 `Window::getControls()` 和 `Canvas::getControls()` 的 mutable 容器入口。
|
||||
- 删除 `Canvas::clearAllControls()`。
|
||||
- `ControlText::lpszFace` 废弃并替换为 `faceName`。
|
||||
- 旧的按文本/旧参数添加 Tab 控件方式不再作为首选接口。
|
||||
- 受影响的文件 / 类 / 函数:
|
||||
- 工程:`StellarX.Core.vcxproj`、`imGui-easyX.vcxproj`、`imGui-easyX.sln`。
|
||||
- 所有权和事务:`CoreTypes.h`、`Control.h/.cpp`、`Window.h/.cpp`、`Canvas.h/.cpp`。
|
||||
- 控件和重绘:`Button.h/.cpp`、`Label.h/.cpp`、`TextBox.cpp`、`Table.cpp`、`Dialog.cpp`、`TabControl.h/.cpp`。
|
||||
- 日志:`SxLog.h/.cpp`。
|
||||
- 示例入口:`z-testDome.cpp`。
|
||||
- 测试:`tests/StellarXTests.cpp`、`tests/UiFixture.cpp`、`scripts/test.ps1`、`scripts/run-ui-tests.ps1`。
|
||||
- 对外 API / 属性变化:[可选]
|
||||
- `ControlHandle addControl(std::unique_ptr<Control>)`
|
||||
- `bool removeControl(ControlHandle)`、`void clearControls()`
|
||||
- `Control* findControl(ControlHandle)`、`std::vector<ControlHandle> getControlHandles() const`
|
||||
- `ControlText::faceName` 替代 `lpszFace`。
|
||||
- `bool SxLogger::setConfig(const SxLogConfig&)`。
|
||||
- `size_t TabControl::addTab(...)`、`bool setActiveIndex(int)`、`tryAddToPage(...)`。
|
||||
|
||||
## 行为对照
|
||||
|
||||
- 变更前:
|
||||
- 回调中直接删除当前或兄弟控件可能使遍历中的裸指针失效。
|
||||
- 空 TabControl 激活和绘制路径可能访问不存在的页签。
|
||||
- 控件几何变化、Tooltip、Dialog 和 Table 的局部 coverage 可能留下旧像素或污染快照。
|
||||
- 日志滚动失败时可能截断原日志;字体名可能引用悬空内存;密码掩码可能按字节而非逻辑字符生成。
|
||||
- 事件循环每轮只处理一条消息并固定休眠,消息积压时响应和重绘收口不稳定。
|
||||
- 变更后:
|
||||
- 控件增删清空在最外层遍历事务结束后提交,回调期间不会释放正在使用的遍历对象。
|
||||
- 空页签路径安全返回,待激活索引在首个页签加入后应用,非法运行期索引返回 `false`。
|
||||
- 几何 setter 先恢复旧 coverage,再登记旧新区域并集;快照、绘制和 coverage 使用一致的矩形语义。
|
||||
- 日志 sink 候选打开成功后才原子替换,滚动重命名失败时继续 append 原文件。
|
||||
- MBCS 文本按逻辑字符处理,Tooltip 由 tick 链驱动到期和清除。
|
||||
- 兼容性说明:不兼容
|
||||
- 迁移说明:[可选]
|
||||
- 调用方应使用稳定 `ControlHandle`,不再保存可变控件容器引用。
|
||||
- 将 `lpszFace` 改为 `faceName`,将旧 Tab 添加路径迁移到索引 API。
|
||||
- 具体示例见 [`MIGRATION.md`](D:/programming/imGUI-easyX/imGui-easyX/开发记录/MIGRATION.md)。
|
||||
|
||||
## 全量缺陷修复矩阵
|
||||
|
||||
| 严重度 | 问题 | 根因与修复 | 主要验证 |
|
||||
|---|---|---|---|
|
||||
| 高 | 回调删除当前控件导致 UAF | 引入稳定 `ControlHandle` 和 `MutationTransaction`;事件、绘制、按钮回调期间的 Add/Remove/Clear 延迟提交。 | self/sibling/remove/clear/add 回调测试;x64 ASan |
|
||||
| 高 | 空 TabControl 激活索引越界 | 空容器绘制不访问末元素;待激活索引延迟到首个页签加入后应用;非法索引返回 `false`。 | 空容器设置 0/大索引并绘制;随后新增页签 |
|
||||
| 中 | Button 快照尺寸恒不匹配 | 提取统一快照矩形计算,比较、保存和 coverage 使用相同宽高。 | hover/click/redraw/hide 像素不变量 |
|
||||
| 中 | 运行时几何 setter 遗留旧像素 | setter 记录旧 coverage、恢复并释放旧快照,再更新矩形并登记旧新区域并集。 | Button/Label/TextBox/Canvas/Table/Tab 移动、缩放、隐藏 |
|
||||
| 中 | Dialog 清理触发额外全量重绘 | `Close()` 统一登记 overlay 关闭,清理只释放对象和快照,Window 在 sweep 后统一重绘。 | 两层异步 Dialog 关闭上层后下层可见、可交互 |
|
||||
| 中 | Table 宽度、列宽和 coverage 不一致 | 计算最小宽度并夹紧请求值;列宽总和严格等于有效内容宽;按 MBCS 边界省略文本。 | 极窄 Table、多列长文本、分页按钮和 coverage |
|
||||
| 中 | 日志滚动失败截断原日志 | 滚动名加入毫秒和递增序号;检查 rename 错误;失败时重新 append 原文件,不执行 trunc。 | 同毫秒滚动、同名文件、无效路径 |
|
||||
| 中 | ControlText 字体名悬空和地址比较 | 使用拥有型 `std::string faceName`;EasyX/GDI 调用使用 `.c_str()`;比较按字符串内容。 | 临时字符串析构、相同内容不同地址、内容变化测量 |
|
||||
| 中 | `setConfig()` 不同步文件 sink | 锁内构造并打开候选 sink,成功后替换;禁用、路径、append 和 rotate 变化同步重开。 | 启用/禁用、切换文件、无效路径和输出一致性 |
|
||||
| 中 | 同名页签可同时激活 | 通过索引排除当前页签,不按标题排除;允许重复标题但只保留一个激活页。 | 两个同名页签来回切换 |
|
||||
| 低 | Tooltip 静止时不会到期显示 | 增加 Control tick 链,Window 传入单调时钟;到期时置脏并登记重绘;禁用和移出统一 hide。 | 假时钟、静止鼠标、到期显示和移出清除 |
|
||||
| 低 | 密码掩码按字节生成 | 复用 MBCS 字符边界遍历,每个逻辑字符生成一个 `*`,非法尾字节仍保证前进。 | ASCII、GBK 双字节、非法尾字节 |
|
||||
| 低 | 事件循环每轮只处理一条消息并固定 Sleep | 提取批处理器,每轮最多 128 条;活动轮不休眠,空闲轮 `Sleep(1)`;批次尾统一收口 resize/重绘。 | 一次注入 1000 条消息;关闭和 resize 顺序 |
|
||||
| 低 | 按文本添加页控件失败时静默销毁 | `tryAddToPage` 失败原样返还 `unique_ptr`;索引重载为首选,文本重载只匹配首个同名页签。 | 不存在标题、非法索引和同名标题 |
|
||||
|
||||
## 验证与落地
|
||||
|
||||
- 验证方式:
|
||||
- 使用 VS Native Unit Test 对所有权、Tab、文本、Tooltip、事件循环和日志逻辑进行测试。
|
||||
- 使用 Debug x64 ASan 对回调生命周期和核心逻辑进行内存安全验证。
|
||||
- 使用真实 EasyX UI Fixture 和 Win32 `SendInput`、`SetWindowPos` 驱动窗口。
|
||||
- 使用 JSON 状态、关键像素、后备缓冲 PNG 和 Fixture 日志进行断言。
|
||||
- 验证结果:
|
||||
- 2026-09-29 执行 `scripts/test.ps1 -Suite NonUI`,Debug x64、Debug x64 ASan、Release x64、Debug Win32 均为 15/15 通过。
|
||||
- x64 ASan 未报告内存错误;KEY1 至 KEY6 均编译、链接通过。
|
||||
- 2026-09-30 为 `scripts/test.ps1` 补充 UTF-8 BOM 后,使用 Windows PowerShell 5.1 重跑 `NonUI`,结果保持通过。
|
||||
- 本轮按约定未启动自动 UI Fixture;用户于 2026-09-30 按 `TESTING.md` 手工完成 KEY1 至 KEY6,暂未发现明显问题。
|
||||
- 2026-07-22 的 UI 六类场景连续三轮通过记录保留为历史证据;当前快照以 2026-09-30 的人工验收结果为准。
|
||||
- TRX 等诊断产物位于未跟踪的 `TestResults/`。
|
||||
- 关联 BUG / Fix:[可选]
|
||||
- 历史重绘和 Tab 相关记录见 `开发记录/BUG`、`开发记录/Fix` 目录。
|
||||
- 本次新增内容以本记录的严重度矩阵统一收口,避免为同一批次重复创建 14 组 BUG/Fix 文档。
|
||||
- Commit:
|
||||
- 当前工作区未提交
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选]
|
||||
- 备注:[可选]
|
||||
- 当前 `Render` 入口复用 UI Fixture,尚未形成完全独立的进程内 Native Render 测试类别。
|
||||
- GUI 仍限定在主线程,字符集仍为 MBCS。
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [`Module-20260915-0001-StellarX核心库与自动化测试基础设施.md`](D:/programming/imGUI-easyX/imGui-easyX/开发记录/模块/Module-20260915-0001-StellarX核心库与自动化测试基础设施.md)
|
||||
- [`MIGRATION.md`](D:/programming/imGUI-easyX/imGui-easyX/开发记录/MIGRATION.md)
|
||||
- [`TESTING.md`](D:/programming/imGUI-easyX/imGui-easyX/开发记录/TESTING.md)
|
||||
@@ -0,0 +1,127 @@
|
||||
# 新增功能模块 / 模块重构
|
||||
|
||||
> 适用场景:新增模块、重大模块重构、核心架构能力演进。
|
||||
> 不适用场景:小接口或轻量功能变更,请使用“功能变更”模板。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- 模块 ID:Module-20260109-0001
|
||||
- 模块名称: SxLog 轻量日志系统
|
||||
- 状态:已完成(历史记录;本次仅归档,未重新验证)
|
||||
- 类型:新增模块
|
||||
- 所属系统 / 子系统: StellarX / Logging / Diagnostics
|
||||
- 版本 / 分支: v3.0.0 / master
|
||||
- 环境: Windows x64 / Visual Studio 2022 / Debug、Release / 控制台模式
|
||||
- 负责人: Ysm-04(历史实现)
|
||||
|
||||
## 背景与目标
|
||||
|
||||
- 背景: Canvas、TabControl、Table 以及快照重绘链路逐渐复杂,仅依靠断点难以还原事件、重绘和状态变化顺序。
|
||||
- 当前痛点:
|
||||
- 缺少统一的日志级别、Tag 过滤和输出渠道。
|
||||
- 分散日志难以控制噪声,也难以保存为可回放证据。
|
||||
- 协作和对外输出需要中英文文本选择能力。
|
||||
- 目标:
|
||||
- 提供统一日志入口和宏封装。
|
||||
- 支持级别控制、Tag 白名单/黑名单、控制台输出、文件输出和按大小滚动。
|
||||
- 提供 `SX_T` / `SxT` 双语文本选择以及作用域耗时记录。
|
||||
- 非目标:[可选]
|
||||
- 不负责字符编码转换。
|
||||
- 不依赖 WinAPI 调试输出通道,不引入第三方日志库。
|
||||
- 不保证高频同步 I/O 场景下无性能影响。
|
||||
|
||||
## 模块边界
|
||||
|
||||
- 职责:
|
||||
- `SxLogger` 负责配置、过滤、格式化与 Sink 分发。
|
||||
- `ConsoleSink`、`FileSink` 负责具体输出。
|
||||
- `SxLogLine` 通过 RAII 完成流式拼接和行提交。
|
||||
- `SxLogScope` 记录作用域耗时。
|
||||
- 不负责什么:
|
||||
- 不负责日志采集服务、网络上报、结构化查询或跨进程聚合。
|
||||
- 不对业务敏感信息进行脱敏。
|
||||
- 外部依赖:
|
||||
- C++ 标准库的流、文件、互斥量、原子变量和时间工具。
|
||||
- `setGBK()` 面向 Windows 控制台环境。
|
||||
- 对外能力 / API:
|
||||
- `SxLogger::Get()`、`setMinLevel()`、`setTagFilter()`、`clearTagFilter()`。
|
||||
- `enableConsole()`、`enableFile()`、`disableFile()`。
|
||||
- `setLanguage()`、`setGBK()`。
|
||||
- `SX_LOG_TRACE`、`SX_LOGD/I/W/E/F`、`SX_TRACE_SCOPE`、`SX_T` / `SxT`。
|
||||
- 关键数据 / 状态:
|
||||
- `SxLogConfig` 保存级别、格式、Tag 策略以及文件配置。
|
||||
- Logger 单例持有配置和 Sink;语言选择使用原子状态。
|
||||
|
||||
## 设计说明
|
||||
|
||||
- 核心流程:
|
||||
1. 日志宏先调用 `shouldLog()` 判断级别、Tag 和可用 Sink。
|
||||
2. 命中条件时构造 `SxLogLine` 并进行流式拼接。
|
||||
3. `SxLogLine` 析构时提交完整日志行。
|
||||
4. `SxLogger::logLine()` 在互斥保护下格式化并分发给控制台和文件 Sink。
|
||||
- 关键对象 / 类关系:
|
||||
- `SxLogger`:中心配置与分发器。
|
||||
- `ILogSink`:输出目标抽象。
|
||||
- `ConsoleSink` / `FileSink`:具体输出实现。
|
||||
- `SxLogLine` / `SxLogScope`:调用端 RAII 辅助对象。
|
||||
- 生命周期:
|
||||
- Logger 为进程内单例;Sink 随配置启用或关闭。
|
||||
- 日志行对象只在命中过滤条件时短暂存在,并在析构时提交。
|
||||
- 事件 / 渲染 / 数据流:[按模块类型填写]
|
||||
- 调用点 -> 宏短路 -> 日志行拼接 -> Logger 过滤/格式化 -> Sink 输出。
|
||||
- 关键不变量:
|
||||
- `shouldLog()` 返回 false 时不应构造日志行或执行字符串拼接。
|
||||
- 单条日志必须作为整体写出,不能被其他线程插入破坏。
|
||||
- 语言切换只选择文本,不进行编码转换。
|
||||
- 未启用任何 Sink 时应直接短路。
|
||||
- 降级 / 回退策略:[可选]
|
||||
- 将最低级别设为 Off,或关闭控制台和文件 Sink。
|
||||
- 必要时通过编译期开关关闭日志宏。
|
||||
|
||||
## 实现与影响
|
||||
|
||||
- 关键实现点:
|
||||
- Tag 过滤使用适合少量 Tag 的线性查找。
|
||||
- 文件输出支持追加和按字节阈值滚动。
|
||||
- 输出阶段使用互斥量保证行级串行;语言状态使用原子变量。
|
||||
- 涉及文件 / 类 / 函数:
|
||||
- 历史路径:`include/StellarX/SxLog.h`
|
||||
- 历史路径:`src/SxLog.cpp`
|
||||
- `SxLogger`、`SxLogConfig`、`ILogSink`、`ConsoleSink`、`FileSink`、`SxLogLine`、`SxLogScope`
|
||||
- 兼容性影响:
|
||||
- 新增独立模块,不改变既有控件公共 API;调用方可选择是否接入。
|
||||
- 性能影响:
|
||||
- 未命中过滤条件时由宏短路降低开销。
|
||||
- 命中时存在格式化、互斥和 I/O 成本;高频 Trace 输出可能成为瓶颈并快速增大日志文件。
|
||||
- 风险点:
|
||||
- 文件权限、文件占用或滚动重命名失败可能影响持续写入。
|
||||
- 控制台代码页差异可能导致中文显示不一致。
|
||||
- 日志内容如果包含敏感数据,需要由调用方自行控制。
|
||||
|
||||
## 测试与验证
|
||||
|
||||
- 测试范围:
|
||||
- 日志级别、Tag 白名单/黑名单、控制台和文件输出、文件滚动、语言切换、不可写路径。
|
||||
- 验证步骤:
|
||||
|
||||
1. 启用控制台并依次输出不同级别,检查格式、级别和语言文本。
|
||||
2. 启用文件输出,验证追加写、Tag 过滤以及不可写路径的返回值。
|
||||
3. 使用较小的滚动阈值持续输出,检查旧文件重命名和新文件续写。
|
||||
|
||||
- 验证结果:
|
||||
- PDF 给出了上述测试方案,但“验证证据”仍是待填占位内容,没有附带日志、截图或性能数据。
|
||||
- 本次只确认模块源码、公共接口、提交和发布记录存在,未重新执行运行时测试。
|
||||
- 已知限制 / 遗留问题:[可选]
|
||||
- 同步互斥和文件 I/O 不适合无约束的高频热路径。
|
||||
- `setGBK()` 只调整终端解释输出的方式,不进行字符串编码转换。
|
||||
|
||||
## 落地信息
|
||||
|
||||
- 关联功能变更 ID:[可选]
|
||||
- 关联 BUG / Fix:[可选]
|
||||
- Commit: `5cb59b36525cde93de83313551296c6458694110`
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选] v3.0.0
|
||||
- 相关文档:[可选]
|
||||
- `StellarX功能_模块变更.pdf` 第 24-28 页。
|
||||
- 历史 `CHANGELOG.md` 的 v3.0.0 条目。
|
||||
@@ -69,11 +69,11 @@
|
||||
- 再将局部矩形映射为世界坐标矩形
|
||||
- 由控件内部受控路径应用运行态矩形
|
||||
- 关键对象 / 类关系:
|
||||
- [`Control`](D:/programming/imGUI-easyX/imGui-easyX/Control.h) 作为统一布局规格与基础解算入口
|
||||
- [`Window`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp) 负责顶层控件统一收口
|
||||
- [`Canvas`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp) 负责容器子控件重映射
|
||||
- [`TabControl`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp) 外层接入统一解算,内部页签栏 / 页面区仍自管
|
||||
- [`Table`](D:/programming/imGUI-easyX/imGui-easyX/Table.cpp) 通过 `LayoutCapability` 显式禁止 `Y` 轴 Stretch
|
||||
- [`Control`](D:/programming/imGUI-easyX/imGui-easyX/include/StellarX/Control.h) 作为统一布局规格与基础解算入口
|
||||
- [`Window`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Window.cpp) 负责顶层控件统一收口
|
||||
- [`Canvas`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Canvas.cpp) 负责容器子控件重映射
|
||||
- [`TabControl`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/TabControl.cpp) 外层接入统一解算,内部页签栏 / 页面区仍自管
|
||||
- [`Table`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Table.cpp) 通过 `LayoutCapability` 显式禁止 `Y` 轴 Stretch
|
||||
- 生命周期:
|
||||
- 设计态矩形 `local*` 在普通 resize / 重排过程中不自动回写
|
||||
- 运行态矩形由统一解算器产出,再通过内部路径应用
|
||||
@@ -102,17 +102,17 @@
|
||||
- 将 `Table` 的 `Y` 轴固定能力边界收回控件自身
|
||||
- 在 `z-testDome.cpp` 增加 `KEY == 5` 布局专项回归
|
||||
- 涉及文件 / 类 / 函数:
|
||||
- [`CoreTypes.h`](D:/programming/imGUI-easyX/imGui-easyX/CoreTypes.h)
|
||||
- [`Control.h`](D:/programming/imGUI-easyX/imGui-easyX/Control.h)
|
||||
- [`Control.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Control.cpp)
|
||||
- [`Canvas.h`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.h)
|
||||
- [`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp)
|
||||
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp)
|
||||
- [`TabControl.h`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.h)
|
||||
- [`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp)
|
||||
- [`Table.h`](D:/programming/imGUI-easyX/imGui-easyX/Table.h)
|
||||
- [`Table.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Table.cpp)
|
||||
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp)
|
||||
- [`CoreTypes.h`](D:/programming/imGUI-easyX/imGui-easyX/include/StellarX/CoreTypes.h)
|
||||
- [`Control.h`](D:/programming/imGUI-easyX/imGui-easyX/include/StellarX/Control.h)
|
||||
- [`Control.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Control.cpp)
|
||||
- [`Canvas.h`](D:/programming/imGUI-easyX/imGui-easyX/include/StellarX/Canvas.h)
|
||||
- [`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Canvas.cpp)
|
||||
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Window.cpp)
|
||||
- [`TabControl.h`](D:/programming/imGUI-easyX/imGui-easyX/include/StellarX/TabControl.h)
|
||||
- [`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/TabControl.cpp)
|
||||
- [`Table.h`](D:/programming/imGUI-easyX/imGui-easyX/include/StellarX/Table.h)
|
||||
- [`Table.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Table.cpp)
|
||||
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/app/z-testDome.cpp)
|
||||
- 兼容性影响:
|
||||
- 向后兼容旧锚点 API
|
||||
- 当前阶段未删除旧 getter / setter
|
||||
|
||||
@@ -67,12 +67,12 @@
|
||||
- 内容驱动控件在自己的受控路径里刷新运行态尺寸。
|
||||
- 局部重绘提交后,由父容器按实际绘制顺序补画 coverage 上方的 overlay 兄弟。
|
||||
- 关键对象 / 类关系:
|
||||
- [`Control`](D:/programming/imGUI-easyX/imGui-easyX/Control.h):几何语义、解算入口、设计基线提交入口。
|
||||
- [`Label`](D:/programming/imGUI-easyX/imGui-easyX/Label.cpp):内容驱动尺寸,`draw()` 只消费运行态矩形。
|
||||
- [`Canvas`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp):管理直接子控件的世界坐标映射与局部 overlay 补画。
|
||||
- [`TabControl`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp):外层接统一解算,内部页签栏 / 页面区继续自管。
|
||||
- [`Table`](D:/programming/imGUI-easyX/imGui-easyX/Table.cpp):当前版本 `X Stretch / Y Fixed`,并保留内部受控结构尺寸基线刷新。
|
||||
- [`Window`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp):顶层托管重绘收口与 overlay 兄弟补画。
|
||||
- [`Control`](D:/programming/imGUI-easyX/imGui-easyX/include/StellarX/Control.h):几何语义、解算入口、设计基线提交入口。
|
||||
- [`Label`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Label.cpp):内容驱动尺寸,`draw()` 只消费运行态矩形。
|
||||
- [`Canvas`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Canvas.cpp):管理直接子控件的世界坐标映射与局部 overlay 补画。
|
||||
- [`TabControl`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/TabControl.cpp):外层接统一解算,内部页签栏 / 页面区继续自管。
|
||||
- [`Table`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Table.cpp):当前版本 `X Stretch / Y Fixed`,并保留内部受控结构尺寸基线刷新。
|
||||
- [`Window`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Window.cpp):顶层托管重绘收口与 overlay 兄弟补画。
|
||||
- 生命周期:
|
||||
- 普通 resize / 父容器重排不自动回写 `local*`。
|
||||
- 显式设计基线提交只通过 `commitCurrentGeometryAsDesignRect()` 或控件内部受控结构刷新点发生。
|
||||
@@ -93,21 +93,21 @@
|
||||
## 实现与影响
|
||||
|
||||
- 关键实现点:
|
||||
- 在 [`Control.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Control.cpp) 增加布局降级日志、能力边界拦截日志、显式设计基线提交日志。
|
||||
- 在 [`Label.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Label.cpp) 收口内容驱动尺寸刷新,并显式关闭双轴 Stretch。
|
||||
- 在 [`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp)、[`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp)、[`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp) 收口 overlay 补画。
|
||||
- 在 [`Table.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Table.cpp) 收口 `Y Fixed`、分页按钮视觉链与页码重绘。
|
||||
- 在 [`Control.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Control.cpp) 增加布局降级日志、能力边界拦截日志、显式设计基线提交日志。
|
||||
- 在 [`Label.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Label.cpp) 收口内容驱动尺寸刷新,并显式关闭双轴 Stretch。
|
||||
- 在 [`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Canvas.cpp)、[`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/TabControl.cpp)、[`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Window.cpp) 收口 overlay 补画。
|
||||
- 在 [`Table.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Table.cpp) 收口 `Y Fixed`、分页按钮视觉链与页码重绘。
|
||||
- 涉及文件 / 类 / 函数:
|
||||
- [`Control.h`](D:/programming/imGUI-easyX/imGui-easyX/Control.h)
|
||||
- [`Control.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Control.cpp)
|
||||
- [`Label.h`](D:/programming/imGUI-easyX/imGui-easyX/Label.h)
|
||||
- [`Label.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Label.cpp)
|
||||
- [`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp)
|
||||
- [`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp)
|
||||
- [`Table.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Table.cpp)
|
||||
- [`TextBox.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TextBox.cpp)
|
||||
- [`Dialog.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Dialog.cpp)
|
||||
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp)
|
||||
- [`Control.h`](D:/programming/imGUI-easyX/imGui-easyX/include/StellarX/Control.h)
|
||||
- [`Control.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Control.cpp)
|
||||
- [`Label.h`](D:/programming/imGUI-easyX/imGui-easyX/include/StellarX/Label.h)
|
||||
- [`Label.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Label.cpp)
|
||||
- [`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Canvas.cpp)
|
||||
- [`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/TabControl.cpp)
|
||||
- [`Table.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Table.cpp)
|
||||
- [`TextBox.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/TextBox.cpp)
|
||||
- [`Dialog.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Dialog.cpp)
|
||||
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Window.cpp)
|
||||
- 兼容性影响:
|
||||
- 对旧锚点 API 保持兼容。
|
||||
- `Label::textStyle` 仍为公开字段,但使用约定更严格。
|
||||
|
||||
@@ -61,12 +61,12 @@
|
||||
- 内容驱动控件在自身受控路径中刷新运行态尺寸;设计基线不得在普通布局过程中自动漂移。
|
||||
- 托管局部重绘按“脏子树提交 -> 直接分支 coverage -> 传递式 overlay 补画”收口,确保嵌套容器、Tooltip、上层兄弟遮挡链闭合。
|
||||
- 关键对象 / 类关系:
|
||||
- [`Control.h`](D:/programming/imGUI-easyX/imGui-easyX/Control.h):统一布局解算、设计基线提交、托管重绘底座
|
||||
- [`Label.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Label.cpp):内容驱动尺寸刷新前移,`draw()` 只消费运行态矩形
|
||||
- [`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp):直接子树映射、脏子树提交、局部 overlay 补画
|
||||
- [`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp):外层统一布局,内部页签栏/页面区自管,绘制顺序与局部提交顺序统一
|
||||
- [`Table.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Table.cpp):当前版本 `X Stretch / Y Fixed`、分页按钮视觉与页码重绘修复
|
||||
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp):顶层托管重绘、传递式 overlay 补画、顶层 coverage 收口
|
||||
- [`Control.h`](D:/programming/imGUI-easyX/imGui-easyX/include/StellarX/Control.h):统一布局解算、设计基线提交、托管重绘底座
|
||||
- [`Label.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Label.cpp):内容驱动尺寸刷新前移,`draw()` 只消费运行态矩形
|
||||
- [`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Canvas.cpp):直接子树映射、脏子树提交、局部 overlay 补画
|
||||
- [`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/TabControl.cpp):外层统一布局,内部页签栏/页面区自管,绘制顺序与局部提交顺序统一
|
||||
- [`Table.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Table.cpp):当前版本 `X Stretch / Y Fixed`、分页按钮视觉与页码重绘修复
|
||||
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Window.cpp):顶层托管重绘、传递式 overlay 补画、顶层 coverage 收口
|
||||
- 生命周期:
|
||||
- 普通 resize / 重排不自动回写 `local*`
|
||||
- 公开 setter 只写运行态,不隐式提交设计基线
|
||||
@@ -94,26 +94,26 @@
|
||||
- 修复 Tab 页签按钮与页面绘制顺序不一致导致 Tooltip 被页面覆盖的问题
|
||||
- 修复重复激活已激活页签时的残影 / 快照链扰动问题
|
||||
- 涉及文件 / 类 / 函数:
|
||||
- [`Control.h`](D:/programming/imGUI-easyX/imGui-easyX/Control.h)
|
||||
- [`Control.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Control.cpp)
|
||||
- [`Label.h`](D:/programming/imGUI-easyX/imGui-easyX/Label.h)
|
||||
- [`Label.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Label.cpp)
|
||||
- [`Button.h`](D:/programming/imGUI-easyX/imGui-easyX/Button.h)
|
||||
- [`Button.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Button.cpp)
|
||||
- [`Canvas.h`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.h)
|
||||
- [`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp)
|
||||
- [`TabControl.h`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.h)
|
||||
- [`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp)
|
||||
- [`Table.h`](D:/programming/imGUI-easyX/imGui-easyX/Table.h)
|
||||
- [`Table.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Table.cpp)
|
||||
- [`TextBox.h`](D:/programming/imGUI-easyX/imGui-easyX/TextBox.h)
|
||||
- [`TextBox.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TextBox.cpp)
|
||||
- [`Dialog.h`](D:/programming/imGUI-easyX/imGui-easyX/Dialog.h)
|
||||
- [`Dialog.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Dialog.cpp)
|
||||
- [`Window.h`](D:/programming/imGUI-easyX/imGui-easyX/Window.h)
|
||||
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp)
|
||||
- [`MessageBox.h`](D:/programming/imGUI-easyX/imGui-easyX/MessageBox.h)
|
||||
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp)
|
||||
- [`Control.h`](D:/programming/imGUI-easyX/imGui-easyX/include/StellarX/Control.h)
|
||||
- [`Control.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Control.cpp)
|
||||
- [`Label.h`](D:/programming/imGUI-easyX/imGui-easyX/include/StellarX/Label.h)
|
||||
- [`Label.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Label.cpp)
|
||||
- [`Button.h`](D:/programming/imGUI-easyX/imGui-easyX/include/StellarX/Button.h)
|
||||
- [`Button.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Button.cpp)
|
||||
- [`Canvas.h`](D:/programming/imGUI-easyX/imGui-easyX/include/StellarX/Canvas.h)
|
||||
- [`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Canvas.cpp)
|
||||
- [`TabControl.h`](D:/programming/imGUI-easyX/imGui-easyX/include/StellarX/TabControl.h)
|
||||
- [`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/TabControl.cpp)
|
||||
- [`Table.h`](D:/programming/imGUI-easyX/imGui-easyX/include/StellarX/Table.h)
|
||||
- [`Table.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Table.cpp)
|
||||
- [`TextBox.h`](D:/programming/imGUI-easyX/imGui-easyX/include/StellarX/TextBox.h)
|
||||
- [`TextBox.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/TextBox.cpp)
|
||||
- [`Dialog.h`](D:/programming/imGUI-easyX/imGui-easyX/include/StellarX/Dialog.h)
|
||||
- [`Dialog.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Dialog.cpp)
|
||||
- [`Window.h`](D:/programming/imGUI-easyX/imGui-easyX/include/StellarX/Window.h)
|
||||
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Window.cpp)
|
||||
- [`MessageBox.h`](D:/programming/imGUI-easyX/imGui-easyX/include/StellarX/MessageBox.h)
|
||||
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/app/z-testDome.cpp)
|
||||
- 兼容性影响:
|
||||
- 旧锚点 API 仍可用,内部实现已完全转到新布局模型
|
||||
- `Label::textStyle` 仍为公开字段,但要求样式修改后显式 `setDirty(true)`
|
||||
|
||||
@@ -72,9 +72,9 @@
|
||||
- 新 API 被调用后,控件自动切到 `AnchorToEdges` 布局模式。
|
||||
- 运行时统一解算继续以 `layoutSpec` 为唯一依据。
|
||||
- 关键对象 / 类关系:
|
||||
- [`Control.h`](D:/programming/imGUI-easyX/imGui-easyX/Control.h):公开布局策略 API 的对外入口。
|
||||
- [`Control.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Control.cpp):新 API 的实现、新旧 API 映射共存规则。
|
||||
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp):`KEY2` 迁移示例与验证入口。
|
||||
- [`Control.h`](D:/programming/imGUI-easyX/imGui-easyX/include/StellarX/Control.h):公开布局策略 API 的对外入口。
|
||||
- [`Control.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Control.cpp):新 API 的实现、新旧 API 映射共存规则。
|
||||
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/app/z-testDome.cpp):`KEY2` 迁移示例与验证入口。
|
||||
- 生命周期:
|
||||
- 新 API 只影响运行态布局策略,不自动提交设计基线。
|
||||
- 设计基线仍由显式提交路径或控件内部受控路径维护。
|
||||
@@ -97,9 +97,9 @@
|
||||
- 保留 `setAnchor(...)` 与旧 getter,不破坏兼容路径。
|
||||
- 以 `KEY2` 位选择区、功能区、显示区和配置区作为迁移样例,验证新 API 的可用性。
|
||||
- 涉及文件 / 类 / 函数:
|
||||
- [`Control.h`](D:/programming/imGUI-easyX/imGui-easyX/Control.h)
|
||||
- [`Control.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Control.cpp)
|
||||
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp)
|
||||
- [`Control.h`](D:/programming/imGUI-easyX/imGui-easyX/include/StellarX/Control.h)
|
||||
- [`Control.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Control.cpp)
|
||||
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/app/z-testDome.cpp)
|
||||
- 兼容性影响:
|
||||
- 兼容旧 `setAnchor(...)` 调用。
|
||||
- 新 API 为新增能力,不影响现有二进制接口使用方式。
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
# 新增功能模块 / 模块重构
|
||||
|
||||
> 适用场景:新增模块、重大模块重构、核心架构能力演进。
|
||||
> 不适用场景:小接口或轻量功能变更,请使用“功能变更”模板。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- 模块 ID:Module-20260915-0001
|
||||
- 模块名称:StellarX 核心库与自动化测试基础设施
|
||||
- 状态:非 UI 与人工 UI 已验证,待提交
|
||||
- 类型:模块重构 / 架构演进
|
||||
- 所属系统 / 子系统:StellarX Core / Unit Test / UI Regression
|
||||
- 版本 / 分支:当前工作区
|
||||
- 环境:Windows + Visual Studio 2022 + MSVC v143 + EasyX
|
||||
- 负责人:Codex 协作修改
|
||||
|
||||
## 背景与目标
|
||||
|
||||
- 背景:
|
||||
- 原应用工程同时承担框架实现、示例入口和回归验证职责,框架代码与测试代码之间缺少明确边界。
|
||||
- 本次全量缺陷修复涉及控件生命周期、重绘、事件循环、MBCS 文本和日志等多个核心模块,需要让应用、单元测试和真实 UI Fixture 复用完全相同的实现。
|
||||
- 当前痛点:
|
||||
- 核心框架源文件分散在应用工程中,难以单独复用和验证。
|
||||
- 仅依赖手工启动窗口,无法稳定覆盖回调期间所有权变化、ASan 内存安全和多配置构建。
|
||||
- UI 控件基于 EasyX 绘制,不是原生子窗口,UIA/WinAppDriver 无法直接按控件语义定位。
|
||||
- 目标:
|
||||
- 将框架实现集中编译为 `StellarX.Core` 静态库。
|
||||
- 建立纯逻辑单元测试、真实 EasyX Fixture 和外部 Win32 UI 驱动三层验证。
|
||||
- 让 Debug、Release、Win32、x64 和 x64 ASan 使用统一的核心实现。
|
||||
- 非目标:[可选]
|
||||
- 不在本次工作中将静态库改为 DLL。
|
||||
- 不迁移到 Unicode,不改变 GUI 只能在主线程使用的约束。
|
||||
- 不将 UI 测试改造成依赖 DPI 或字体的全图 golden image 测试。
|
||||
|
||||
## 模块边界
|
||||
|
||||
- 职责:
|
||||
- `StellarX.Core.vcxproj` 编译 `Window`、`Control`、`Canvas`、各类控件、Dialog、TabControl 和日志实现。
|
||||
- 主应用、Native Unit Test 和 UI Fixture 通过 `ProjectReference` 使用同一份核心实现。
|
||||
- `scripts/test.ps1` 统一组织 Unit、Render、UI、All 和 CI 测试入口。
|
||||
- `scripts/run-ui-tests.ps1` 负责启动 Fixture、注入鼠标输入、等待结果并保存诊断文件。
|
||||
- 不负责什么:
|
||||
- 不负责最终应用资源、业务示例和安装包制作。
|
||||
- 不把 `StellarX.Core.lib` 作为普通 GUI 应用用户的运行时 DLL。
|
||||
- 不替代 EasyX、Visual C++ Runtime 等外部依赖的发布判断。
|
||||
- 外部依赖:
|
||||
- Visual Studio 2022、MSVC v143、Windows SDK。
|
||||
- EasyX 图形库。
|
||||
- Visual Studio Native Unit Test Framework。
|
||||
- UI 套件需要可交互的 Windows 桌面,并会短暂切换前台窗口和移动鼠标。
|
||||
- 对外能力 / API:
|
||||
- 核心库对外 API 通过 `StellarX.h` 汇总。
|
||||
- `StellarX.Core` 本身是构建期静态库,不新增运行时动态加载接口。
|
||||
- 测试入口通过 PowerShell 参数和 Fixture 场景提供,不属于框架公共 API。
|
||||
- 关键数据 / 状态:
|
||||
- `StellarX.Core.lib`:框架实现的静态链接产物。
|
||||
- `StellarX.Tests.dll`:Native Unit Test 产物。
|
||||
- `StellarX.UiFixture.exe`:真实 EasyX UI 场景进程。
|
||||
- `TestResults/`:TRX、JSON、PNG 和日志诊断产物。
|
||||
|
||||
## 设计说明
|
||||
|
||||
- 核心流程:
|
||||
1. `StellarX.Core.vcxproj` 编译框架头文件和源文件。
|
||||
2. `imGui-easyX.vcxproj` 通过 `ProjectReference` 链接核心静态库,应用入口保留在 `z-testDome.cpp`。
|
||||
3. `StellarX.Tests` 和 `StellarX.UiFixture` 同样引用核心工程,避免测试复制另一份框架代码。
|
||||
4. Release 构建将核心代码静态链接进目标可执行文件;运行时不要求单独加载 `StellarX.Core.dll`。
|
||||
- 关键对象 / 类关系:
|
||||
- `StellarX.Core.vcxproj`:核心实现的构建边界。
|
||||
- `Window / Canvas / Control`:窗口托管、控件所有权、事件和重绘基础。
|
||||
- `SxEventLoopInternal.h`:批量事件处理测试辅助。
|
||||
- `SxTextInternal.h`:MBCS 字符边界和密码掩码测试辅助。
|
||||
- `StellarX.Tests`:不创建窗口的逻辑测试。
|
||||
- `StellarX.UiFixture`:创建确定性 EasyX 窗口并导出状态和像素证据。
|
||||
- 生命周期:
|
||||
- 核心库在构建阶段生成,静态链接进应用或测试可执行文件。
|
||||
- Unit 测试进程独立运行;涉及 UAF 的回归可在 ASan 配置中隔离执行。
|
||||
- UI Fixture 按场景创建和关闭图形上下文,失败时由脚本保存证据并发送 `WM_CLOSE`。
|
||||
- 事件 / 渲染 / 数据流:
|
||||
- 逻辑测试使用 FakeControl、假时钟、假消息源和临时目录验证状态转移。
|
||||
- UI 驱动程序通过 `SendInput` 注入鼠标事件,通过 `SetWindowPos` 驱动窗口变化。
|
||||
- Fixture 导出 JSON 状态和 EasyX 后备缓冲像素,脚本按关键像素不变量判断结果。
|
||||
- 关键不变量:
|
||||
- 应用、逻辑测试和 UI Fixture 必须链接同一份核心实现。
|
||||
- ASan 产物与普通 Debug 产物使用独立目录。
|
||||
- UI 场景串行执行,避免多个 EasyX 图形上下文争用桌面。
|
||||
- UI 断言不依赖 DPI、字体抗锯齿和完整截图的像素一致性。
|
||||
- 降级 / 回退策略:[可选]
|
||||
- 没有交互桌面时,CI 只运行逻辑测试;设置 `STELLARX_UI_TESTS=1` 且环境满足条件时才执行 UI。
|
||||
- 全局 resize、Dialog 开关等场景保留整场景重绘兜底。
|
||||
|
||||
## 实现与影响
|
||||
|
||||
- 关键实现点:
|
||||
- 新增 `StellarX.Core` 静态库工程和解决方案项目引用。
|
||||
- 新增 `StellarX.Tests`、`StellarX.UiFixture`、PowerShell 测试脚本和诊断输出约定。
|
||||
- 新增 `SxEventLoopInternal.h`、`SxTextInternal.h` 两个仅供内部实现和测试使用的头文件。
|
||||
- 核心控件采用稳定句柄、延迟变更事务和统一 geometry/coverage 处理。
|
||||
- 涉及文件 / 类 / 函数:
|
||||
- 工程:`StellarX.Core.vcxproj`、`imGui-easyX.vcxproj`、`imGui-easyX.sln`。
|
||||
- 核心:`Control`、`Window`、`Canvas`、`Button`、`Label`、`TextBox`、`Table`、`Dialog`、`TabControl`、`SxLogger`。
|
||||
- 测试:`tests/StellarXTests.cpp`、`tests/UiFixture.cpp`、`scripts/test.ps1`、`scripts/run-ui-tests.ps1`。
|
||||
- 兼容性影响:
|
||||
- 工程内部从直接编译框架源文件调整为引用 `StellarX.Core`。
|
||||
- 本阶段同时包含破坏性公共 API 调整,迁移方式见 [`MIGRATION.md`](D:/programming/imGUI-easyX/imGui-easyX/开发记录/MIGRATION.md)。
|
||||
- 性能影响:
|
||||
- 事件循环单轮最多处理 128 条消息,活动轮不固定休眠,空闲轮短暂休眠。
|
||||
- 静态库不引入运行时动态分发开销;托管重绘仍按覆盖范围选择性提交并保留全局兜底。
|
||||
- 风险点:
|
||||
- EasyX UI 测试依赖前台交互桌面,不能作为无桌面 CI 的默认测试。
|
||||
- 当前 `Render` 入口复用 UI Fixture,尚未形成完全独立的进程内 Native Render 测试类别。
|
||||
- 编译仍存在 EasyX `C4201`、未使用变量和名称遮蔽等非阻断警告。
|
||||
|
||||
## 测试与验证
|
||||
|
||||
- 测试范围:
|
||||
- 所有权事务、空 TabControl、Tab 索引、字体拥有权、MBCS 掩码、Tooltip tick、事件批处理和日志配置。
|
||||
- Button、Label、TextBox、Canvas、Table、Dialog、TabControl 的真实 EasyX 重绘和 coverage 不变量。
|
||||
- Debug x64、Debug x64 ASan、Release x64、Debug Win32,以及 KEY1 至 KEY6 编译。
|
||||
- 验证步骤:
|
||||
|
||||
1. 使用 `scripts/test.ps1 -Suite NonUI` 验证四套逻辑配置、ASan 和 KEY1 至 KEY6 编译。
|
||||
2. 按 `TESTING.md` 的人工 UI 清单验证 KEY1、KEY2、KEY4、KEY5、KEY6。
|
||||
3. 检查 TRX 和人工验收记录;只有明确允许自动 UI 时才使用 `scripts/test.ps1 -Suite All`。
|
||||
|
||||
- 验证结果:
|
||||
- 2026-09-29 的 Debug x64、Debug x64 ASan、Release x64、Debug Win32 均为 15/15 通过。
|
||||
- x64 ASan 未报告内存错误;KEY1 至 KEY6 均编译、链接通过。
|
||||
- 2026-09-30 为 `scripts/test.ps1` 补充 UTF-8 BOM 后,Windows PowerShell 5.1 下的 `NonUI` 全矩阵复验通过。
|
||||
- 本轮未启动自动 UI Fixture;用户于 2026-09-30 手工完成 KEY1 至 KEY6,暂未发现明显问题。
|
||||
- 2026-07-22 的 UI 六类场景三轮通过记录保留为历史证据;当前快照以 2026-09-30 的人工验收结果为准。
|
||||
- 已知限制 / 遗留问题:[可选]
|
||||
- `Render` 当前复用单轮真实 UI Fixture;后续可单独建立进程内渲染测试类别。
|
||||
- UI 自动化仍需要已解锁的交互式桌面。
|
||||
- 本次未清理已有非阻断编译警告。
|
||||
|
||||
## 落地信息
|
||||
|
||||
- 关联功能变更 ID:[可选]
|
||||
- `Feature-20260915-0001`
|
||||
- 关联 BUG / Fix:[可选]
|
||||
- 本次以全量缺陷修复矩阵统一记录,未为每个历史问题重新创建重复 BUG/Fix 文档。
|
||||
- Commit:
|
||||
- 当前工作区未提交
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选]
|
||||
- 相关文档:[可选]
|
||||
- [`TESTING.md`](D:/programming/imGUI-easyX/imGui-easyX/开发记录/TESTING.md)
|
||||
- [`MIGRATION.md`](D:/programming/imGUI-easyX/imGui-easyX/开发记录/MIGRATION.md)
|
||||
Reference in New Issue
Block a user