Skip to content

Commit c4df9cb

Browse files
asklarCopilot
andcommitted
Remove unreliable layout offset heuristic, add DPI scaling
The ComputeLayoutOffsets heuristic produced wrong positions for elements in Grid, Canvas, and other non-StackPanel containers, causing annotation rectangles to be misaligned with actual UI elements. Changes: - Remove ComputeLayoutOffsets and layout field collection from TAP DLL - Add ApplyDpiScaling: scale XAML logical pixel dimensions by the window DPI factor (dpi/96) so they match the physical pixel space used by GetWindowRect and the screenshot capture bitmap - Remove unused IVisualTreeService2 member (GetProperty approach didn't work for Vector3 value types) - Keep the improved size-based bridge-to-XAML matching Without per-element offsets from ActualOffset (WinUI3 serializes Vector3 as '0'), elements within a XAML island share the bridge origin position. This is a known WinUI3 diagnostics limitation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 65265c5 commit c4df9cb

1 file changed

Lines changed: 32 additions & 83 deletions

File tree

src/tap/lvt_tap.cpp

Lines changed: 32 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,6 @@ struct TreeNode {
6161
double width = 0, height = 0;
6262
double offsetX = 0, offsetY = 0;
6363
bool hasBounds = false;
64-
// Layout info for offset computation
65-
int orientation = -1; // -1=unknown, 0=vertical, 1=horizontal (StackPanel)
66-
double spacing = 0;
67-
double marginLeft = 0, marginTop = 0, marginRight = 0, marginBottom = 0;
6864
};
6965

7066
class LvtTap;
@@ -84,7 +80,6 @@ class LvtTap : public IObjectWithSite, public IVisualTreeServiceCallback2 {
8480

8581
public:
8682
IVisualTreeService* m_vts = nullptr;
87-
IVisualTreeService2* m_vts2 = nullptr;
8883
static constexpr UINT WM_COLLECT_BOUNDS = WM_USER + 100;
8984

9085
public:
@@ -170,14 +165,7 @@ class LvtTap : public IObjectWithSite, public IVisualTreeServiceCallback2 {
170165
return S_OK;
171166
}
172167

173-
// Try to get IVisualTreeService2 for GetPropertyIndex/GetProperty (Vector3 workaround)
174-
hr = m_vts->QueryInterface(__uuidof(IVisualTreeService2), (void**)&m_vts2);
175-
if (FAILED(hr)) {
176-
LogMsg("QI for IVisualTreeService2 failed (non-fatal): 0x%08X", hr);
177-
m_vts2 = nullptr;
178-
}
179-
180-
m_diag = diag; // Keep reference for GetIInspectableFromHandle
168+
m_diag = diag;
181169

182170
// Create a message-only window on the UI thread for dispatching
183171
// GetPropertyValuesChain calls (which have thread affinity).
@@ -232,9 +220,8 @@ class LvtTap : public IObjectWithSite, public IVisualTreeServiceCallback2 {
232220
SendMessageW(self->m_msgWnd, WM_COLLECT_BOUNDS, 0,
233221
reinterpret_cast<LPARAM>(self));
234222
}
235-
// Compute layout-based offsets for Panel children (workaround for
236-
// WinUI3 ActualOffset serialization returning "0" for Vector3 types)
237-
self->ComputeLayoutOffsets();
223+
// Apply DPI scaling (XAML logical pixels → physical pixels)
224+
self->ApplyDpiScaling();
238225
self->SerializeAndSend();
239226
self->m_vts->UnadviseVisualTreeChange(cb);
240227
}
@@ -302,7 +289,7 @@ class LvtTap : public IObjectWithSite, public IVisualTreeServiceCallback2 {
302289
}
303290

304291
// Collect bounds for a single node — isolated for SEH compatibility
305-
static void CollectBoundsForNode(IVisualTreeService* vts, IVisualTreeService2* /*vts2*/,
292+
static void CollectBoundsForNode(IVisualTreeService* vts,
306293
TreeNode& node, InstanceHandle handle,
307294
bool logDetail) {
308295
if (logDetail) LogMsg(" CollectBoundsForNode ENTER handle=%llu", (unsigned long long)handle);
@@ -339,19 +326,6 @@ class LvtTap : public IObjectWithSite, public IVisualTreeServiceCallback2 {
339326
foundOffset = true;
340327
}
341328
}
342-
// Collect layout properties for offset computation
343-
// Only take the first value (most specific in the property chain)
344-
if (name == L"Orientation" && !value.empty() && node.orientation < 0) {
345-
node.orientation = _wtoi(value.c_str());
346-
} else if (name == L"Spacing" && !value.empty() && node.spacing == 0) {
347-
node.spacing = _wtof(value.c_str());
348-
} else if (name == L"Margin" && !value.empty()) {
349-
double l = 0, t = 0, r = 0, b = 0;
350-
if (swscanf_s(value.c_str(), L"%lf,%lf,%lf,%lf", &l, &t, &r, &b) >= 2) {
351-
node.marginLeft = l; node.marginTop = t;
352-
node.marginRight = r; node.marginBottom = b;
353-
}
354-
}
355329
if (props[i].Type) SysFreeString(props[i].Type);
356330
if (props[i].DeclaringType) SysFreeString(props[i].DeclaringType);
357331
if (props[i].ValueType) SysFreeString(props[i].ValueType);
@@ -368,11 +342,11 @@ class LvtTap : public IObjectWithSite, public IVisualTreeServiceCallback2 {
368342
}
369343

370344
// SEH wrapper for single-node bounds collection (cannot use __try with C++ objects)
371-
static int CollectBoundsForNodeSEH(IVisualTreeService* vts, IVisualTreeService2* vts2,
345+
static int CollectBoundsForNodeSEH(IVisualTreeService* vts,
372346
TreeNode& node, InstanceHandle handle,
373347
bool logDetail) {
374348
__try {
375-
CollectBoundsForNode(vts, vts2, node, handle, logDetail);
349+
CollectBoundsForNode(vts, node, handle, logDetail);
376350
return 0;
377351
} __except(EXCEPTION_EXECUTE_HANDLER) {
378352
return GetExceptionCode();
@@ -386,7 +360,7 @@ class LvtTap : public IObjectWithSite, public IVisualTreeServiceCallback2 {
386360
int idx = 0;
387361
for (auto& [handle, node] : m_nodes) {
388362
bool logDetail = false; // Set to true for debugging
389-
int code = CollectBoundsForNodeSEH(vts, m_vts2, node, handle, logDetail);
363+
int code = CollectBoundsForNodeSEH(vts, node, handle, logDetail);
390364
if (code != 0) {
391365
LogMsg("GetPropertyValuesChain crashed for handle %llu: 0x%08X",
392366
(unsigned long long)handle, code);
@@ -397,58 +371,34 @@ class LvtTap : public IObjectWithSite, public IVisualTreeServiceCallback2 {
397371
LogMsg("CollectBounds: collected bounds for %d/%zu nodes", collected, m_nodes.size());
398372
}
399373

400-
// Compute layout-based offsets for children of StackPanel/Panel containers.
401-
// WinUI3's XAML diagnostics can't serialize Vector3 ActualOffset values, so we
402-
// approximate child positions by accumulating widths (horizontal) or heights (vertical)
403-
// along the stacking axis of the parent container.
404-
void ComputeLayoutOffsets() {
405-
int computed = 0;
406-
for (auto& [handle, node] : m_nodes) {
407-
if (node.childHandles.empty()) continue;
408-
// Determine orientation: StackPanel has explicit Orientation,
409-
// other Panels (CommandBar, MenuBar, etc.) may default to horizontal
410-
int orient = node.orientation;
411-
if (orient < 0) {
412-
// Heuristic: types containing "Bar", "Toolbar", "CommandBar" → horizontal
413-
// "StackPanel" with no explicit orientation → vertical (default)
414-
auto& t = node.type;
415-
if (t.find(L"StackPanel") != std::wstring::npos) {
416-
orient = 0; // vertical by default
417-
} else if (t.find(L"Bar") != std::wstring::npos ||
418-
t.find(L"Toolbar") != std::wstring::npos) {
419-
orient = 1; // horizontal
420-
}
374+
// Apply DPI scaling to XAML element dimensions.
375+
// XAML ActualWidth/ActualHeight are in logical pixels; bridge positions from
376+
// GetWindowRect are in physical pixels. Scale dimensions to match.
377+
void ApplyDpiScaling() {
378+
HWND hwnd = nullptr;
379+
EnumWindows([](HWND h, LPARAM lp) -> BOOL {
380+
DWORD pid = 0;
381+
GetWindowThreadProcessId(h, &pid);
382+
if (pid == GetCurrentProcessId() && IsWindowVisible(h)) {
383+
*reinterpret_cast<HWND*>(lp) = h;
384+
return FALSE;
421385
}
422-
if (orient < 0) continue; // unknown container, skip
423-
424-
double cursor = 0;
425-
for (size_t ci = 0; ci < node.childHandles.size(); ci++) {
426-
auto cit = m_nodes.find(node.childHandles[ci]);
427-
if (cit == m_nodes.end()) continue;
428-
auto& child = cit->second;
429-
if (!child.hasBounds) continue;
430-
431-
// Only set offset if not already computed
432-
if (child.offsetX == 0 && child.offsetY == 0) {
433-
if (orient == 1) { // horizontal
434-
child.offsetX = cursor + child.marginLeft;
435-
child.offsetY = child.marginTop;
436-
} else { // vertical
437-
child.offsetX = child.marginLeft;
438-
child.offsetY = cursor + child.marginTop;
439-
}
440-
computed++;
441-
}
386+
return TRUE;
387+
}, reinterpret_cast<LPARAM>(&hwnd));
442388

443-
// Advance cursor
444-
if (orient == 1) {
445-
cursor += child.marginLeft + child.width + child.marginRight + node.spacing;
446-
} else {
447-
cursor += child.marginTop + child.height + child.marginBottom + node.spacing;
448-
}
449-
}
389+
if (!hwnd) return;
390+
UINT dpi = GetDpiForWindow(hwnd);
391+
if (dpi == 0 || dpi == 96) return;
392+
393+
double scale = static_cast<double>(dpi) / 96.0;
394+
LogMsg("ApplyDpiScaling: DPI=%u scale=%.2f", dpi, scale);
395+
for (auto& [handle, node] : m_nodes) {
396+
if (!node.hasBounds) continue;
397+
node.width *= scale;
398+
node.height *= scale;
399+
node.offsetX *= scale;
400+
node.offsetY *= scale;
450401
}
451-
LogMsg("ComputeLayoutOffsets: computed %d offsets", computed);
452402
}
453403

454404
// Called on the UI thread via SendMessage from the worker thread
@@ -540,7 +490,6 @@ class LvtTap : public IObjectWithSite, public IVisualTreeServiceCallback2 {
540490

541491
~LvtTap() {
542492
if (m_msgWnd) DestroyWindow(m_msgWnd);
543-
if (m_vts2) m_vts2->Release();
544493
if (m_vts) m_vts->Release();
545494
if (m_diag) m_diag->Release();
546495
if (m_site) m_site->Release();

0 commit comments

Comments
 (0)