Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ jobs:
- name: Build managed Avalonia tree walker
run: dotnet build src/plugin_avalonia/LvtAvaloniaTreeWalker/LvtAvaloniaTreeWalker.csproj -c Release

- name: Build managed WinForms TAP assembly
run: dotnet build src/tap_winforms/LvtWinFormsTap.csproj -c Release

- name: Build
run: cmake --build build

Expand Down Expand Up @@ -63,4 +66,7 @@ jobs:
build/lvt_tap_x64.dll
build/lvt_wpf_tap_x64.dll
build/LvtWpfTap.dll
build/lvt_winforms_tap_x64.dll
build/LvtWinFormsTap.dll
build/LvtWinFormsTap.runtimeconfig.json
build/plugins/
59 changes: 59 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ add_executable(lvt
src/providers/winui3_provider.cpp
src/providers/wpf_provider.cpp
src/providers/wpf_inject.cpp
src/providers/winforms_provider.cpp
src/providers/winforms_inject.cpp
src/providers/xaml_diag_common.cpp
)

Expand Down Expand Up @@ -192,6 +194,46 @@ add_custom_command(TARGET lvt_wpf_tap POST_BUILD
COMMENT "Copying managed WPF tree walker assembly"
)

# WinForms TAP DLL — native loader injected into WinForms target process
add_library(lvt_winforms_tap SHARED
src/tap_winforms/winforms_tap_native.cpp
src/tap_winforms/winforms_tap_native.def
)
target_compile_definitions(lvt_winforms_tap PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX)
target_include_directories(lvt_winforms_tap PRIVATE src)
target_link_libraries(lvt_winforms_tap PRIVATE ole32)
set_property(TARGET lvt_winforms_tap PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")

set_target_properties(lvt_winforms_tap PROPERTIES
OUTPUT_NAME "lvt_winforms_tap_${TAP_ARCH_SUFFIX}"
RUNTIME_OUTPUT_DIRECTORY $<TARGET_FILE_DIR:lvt>
)

set(LVT_WINFORMS_MANAGED_DLL "${CMAKE_SOURCE_DIR}/src/tap_winforms/bin/Release/LvtWinFormsTap.dll")
add_custom_command(
OUTPUT "${LVT_WINFORMS_MANAGED_DLL}"
COMMAND dotnet build "${CMAKE_SOURCE_DIR}/src/tap_winforms/LvtWinFormsTap.csproj" -c Release --nologo
DEPENDS
"${CMAKE_SOURCE_DIR}/src/tap_winforms/LvtWinFormsTap.csproj"
"${CMAKE_SOURCE_DIR}/src/tap_winforms/WinFormsTreeWalker.cs"
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
COMMENT "Building managed WinForms tree walker assembly"
VERBATIM
)
add_custom_target(lvt_winforms_managed ALL
DEPENDS "${LVT_WINFORMS_MANAGED_DLL}"
)
add_dependencies(lvt_winforms_tap lvt_winforms_managed)
add_custom_command(TARGET lvt_winforms_tap POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${LVT_WINFORMS_MANAGED_DLL}"
"$<TARGET_FILE_DIR:lvt>/LvtWinFormsTap.dll"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CMAKE_SOURCE_DIR}/src/tap_winforms/LvtWinFormsTap.runtimeconfig.json"
"$<TARGET_FILE_DIR:lvt>/LvtWinFormsTap.runtimeconfig.json"
COMMENT "Copying managed WinForms tree walker assembly"
)

# Avalonia plugin DLL — runtime-loaded plugin for Avalonia UI framework support
add_library(lvt_avalonia_plugin SHARED
src/plugin_avalonia/lvt_avalonia_plugin.cpp
Expand Down Expand Up @@ -291,6 +333,8 @@ add_executable(lvt_unit_tests
src/providers/winui3_provider.cpp
src/providers/wpf_provider.cpp
src/providers/wpf_inject.cpp
src/providers/winforms_provider.cpp
src/providers/winforms_inject.cpp
src/providers/xaml_diag_common.cpp
)
target_include_directories(lvt_unit_tests PRIVATE src)
Expand Down Expand Up @@ -352,6 +396,18 @@ add_custom_target(avalonia_test_app ALL
file(TO_NATIVE_PATH "${AVALONIA_SAMPLE_OUTPUT_DIR}/AvaloniaTestApp.exe" AVALONIA_SAMPLE_EXE_NATIVE)
string(REPLACE "\\" "\\\\" AVALONIA_SAMPLE_EXE_ESCAPED "${AVALONIA_SAMPLE_EXE_NATIVE}")

# WinForms sample app used by integration tests
set(WINFORMS_SAMPLE_PROJECT "${CMAKE_SOURCE_DIR}/tests/apps/WinFormsSample/WinFormsSample.csproj")
set(WINFORMS_SAMPLE_OUTPUT_DIR "${CMAKE_BINARY_DIR}/tests/apps/WinFormsSample")
add_custom_target(winforms_sample_app ALL
COMMAND dotnet build "${WINFORMS_SAMPLE_PROJECT}" -c Release -o "${WINFORMS_SAMPLE_OUTPUT_DIR}" --nologo
BYPRODUCTS "${WINFORMS_SAMPLE_OUTPUT_DIR}/WinFormsSample.exe"
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
COMMENT "Building WinForms sample app"
)
file(TO_NATIVE_PATH "${WINFORMS_SAMPLE_OUTPUT_DIR}/WinFormsSample.exe" WINFORMS_SAMPLE_EXE_NATIVE)
string(REPLACE "\\" "\\\\" WINFORMS_SAMPLE_EXE_ESCAPED "${WINFORMS_SAMPLE_EXE_NATIVE}")

# Integration tests — require a running Notepad instance
add_executable(lvt_integration_tests
tests/integration_tests.cpp
Expand All @@ -363,6 +419,7 @@ target_compile_definitions(lvt_integration_tests PRIVATE
WPF_SAMPLE_EXE_PATH="${WPF_SAMPLE_EXE_ESCAPED}"
WINUI3_SAMPLE_EXE_PATH="${WINUI3_SAMPLE_EXE_ESCAPED}"
AVALONIA_SAMPLE_EXE_PATH="${AVALONIA_SAMPLE_EXE_ESCAPED}"
WINFORMS_SAMPLE_EXE_PATH="${WINFORMS_SAMPLE_EXE_ESCAPED}"
)
target_link_libraries(lvt_integration_tests PRIVATE
GTest::gtest GTest::gtest_main
Expand All @@ -372,12 +429,14 @@ target_link_libraries(lvt_integration_tests PRIVATE
target_link_options(lvt_integration_tests PRIVATE "/MANIFEST:NO")
add_dependencies(lvt_integration_tests
lvt
lvt_winforms_tap
lvt_avalonia_plugin
lvt_avalonia_tap
lvt_avalonia_tree_walker
avalonia_test_app
wpf_sample_app
winui3_sample_app
winforms_sample_app
)
add_test(NAME integration_tests COMMAND lvt_integration_tests)

Expand Down
10 changes: 8 additions & 2 deletions build.cmd
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,25 @@ REM --- .NET projects (must build before CMake) ---
echo.
echo === Building .NET projects ===

echo [1/2] LvtWpfTap (net48)...
echo [1/3] LvtWpfTap (net48)...
dotnet build src\tap_wpf\LvtWpfTap.csproj -c Release -v:q --nologo
if errorlevel 1 (
echo WARNING: LvtWpfTap build failed (WPF TAP will be unavailable)
)

echo [2/2] LvtAvaloniaTreeWalker (net8.0)...
echo [2/3] LvtAvaloniaTreeWalker (net8.0)...
dotnet restore src\plugin_avalonia\LvtAvaloniaTreeWalker\LvtAvaloniaTreeWalker.csproj -v:q --nologo
dotnet publish src\plugin_avalonia\LvtAvaloniaTreeWalker\LvtAvaloniaTreeWalker.csproj -c Release -v:q --nologo
if errorlevel 1 (
echo WARNING: LvtAvaloniaTreeWalker build failed (Avalonia plugin will be unavailable)
)

echo [3/3] LvtWinFormsTap (net48)...
dotnet build src\tap_winforms\LvtWinFormsTap.csproj -c Release -v:q --nologo
if errorlevel 1 (
echo WARNING: LvtWinFormsTap build failed (WinForms enrichment will be unavailable)
)

REM --- CMake configure + build ---

echo.
Expand Down
17 changes: 17 additions & 0 deletions src/framework_detector.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ std::string framework_to_string(Framework f) {
case Framework::Xaml: return "xaml";
case Framework::WinUI3: return "winui3";
case Framework::Wpf: return "wpf";
case Framework::WinForms: return "winforms";
case Framework::Plugin: return "plugin";
}
return "unknown";
Expand All @@ -37,6 +38,7 @@ struct DetectData {
bool hasWinUI3 = false;
bool hasXaml = false;
bool hasWpf = false;
bool hasWinForms = false;
};

static BOOL CALLBACK detect_child_proc(HWND hwnd, LPARAM lParam) {
Expand Down Expand Up @@ -66,6 +68,10 @@ static BOOL CALLBACK detect_child_proc(HWND hwnd, LPARAM lParam) {
data->hasWpf = true;
}

if (wcsstr(cls, L"WindowsForms10.")) {
data->hasWinForms = true;
}

return TRUE;
}

Expand Down Expand Up @@ -143,6 +149,9 @@ std::vector<FrameworkInfo> detect_frameworks(HWND hwnd, DWORD pid) {
if (wcsstr(topCls, L"HwndWrapper[")) {
data.hasWpf = true;
}
if (wcsstr(topCls, L"WindowsForms10.")) {
data.hasWinForms = true;
}

EnumChildWindows(hwnd, detect_child_proc, reinterpret_cast<LPARAM>(&data));
if (data.hasComCtl) {
Expand All @@ -168,6 +177,7 @@ std::vector<FrameworkInfo> detect_frameworks(HWND hwnd, DWORD pid) {
bool detectedWinUI3 = false;
bool detectedXaml = false;
bool detectedWpf = false;
bool detectedWinForms = false;
if (pid) {
auto winui = detect_module(pid, L"Microsoft.UI.Xaml.dll");
if (winui.found) {
Expand All @@ -188,6 +198,11 @@ std::vector<FrameworkInfo> detect_frameworks(HWND hwnd, DWORD pid) {
result.push_back({Framework::Wpf, wpf.version});
detectedWpf = true;
}
auto winforms = detect_module(pid, L"System.Windows.Forms.dll");
if (winforms.found) {
result.push_back({Framework::WinForms, winforms.version});
detectedWinForms = true;
}
}

// Class-name fallback (works when module enumeration fails)
Expand All @@ -197,6 +212,8 @@ std::vector<FrameworkInfo> detect_frameworks(HWND hwnd, DWORD pid) {
result.push_back({Framework::Xaml, {}});
if (!detectedWpf && data.hasWpf)
result.push_back({Framework::Wpf, {}});
if (!detectedWinForms && data.hasWinForms)
result.push_back({Framework::WinForms, {}});

// Plugin-provided framework detection
auto pluginFws = detect_plugin_frameworks(hwnd, pid);
Expand Down
1 change: 1 addition & 0 deletions src/framework_detector.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ enum class Framework {
Xaml,
WinUI3,
Wpf,
WinForms,
Plugin, // Plugin-provided framework (name in FrameworkInfo::name)
};

Expand Down
Loading
Loading