From 15bea7d3956a5860e17b03d9f64b9fd74418cf38 Mon Sep 17 00:00:00 2001 From: Julian-MW Date: Fri, 6 Nov 2026 20:29:31 +0100 Subject: [PATCH 1/6] =?UTF-8?q?=F0=9F=90=9B=20fix(activex):=20paint=20flic?= =?UTF-8?q?ker-free=20frames=20offscreen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Compose OnDraw frames in a resizable DIB-section frame buffer and present them with a single BitBlt, so the visible surface never shows the background erase or a partially drawn frame - Suppress WM_ERASEBKGND because OnDraw paints an opaque frame over the whole bounds - Reword the OpenGL fallback diagnostic to name the preview limitation and the GDI fallback - Add FrameBuffer unit tests and update the diagnostic assertions --- .ai/memory/architecture/activex.md | 3 + src/MapWinGIS.ActiveX/FrameBuffer.cpp | 105 ++++++++++++++++ src/MapWinGIS.ActiveX/FrameBuffer.h | 54 +++++++++ .../MapWinGIS.ActiveX.Impl.vcxproj | 2 + .../MapWinGIS.ActiveX.vcxproj | 1 + .../FrameBufferTests.cpp | 112 ++++++++++++++++++ .../MapWinGIS.ActiveX.Tests.vcxproj | 1 + 7 files changed, 278 insertions(+) create mode 100644 src/MapWinGIS.ActiveX/FrameBuffer.cpp create mode 100644 src/MapWinGIS.ActiveX/FrameBuffer.h create mode 100644 tests/MapWinGIS.ActiveX.Tests/FrameBufferTests.cpp diff --git a/.ai/memory/architecture/activex.md b/.ai/memory/architecture/activex.md index ae641de..b637630 100644 --- a/.ai/memory/architecture/activex.md +++ b/.ai/memory/architecture/activex.md @@ -26,6 +26,9 @@ The ActiveX rendering coordinator selects a backend for a Core render scene. GDI is the CPU fallback; OpenGL has a narrower supported-scene range and can fall back to GDI. Backends consume Core-produced geometry and do not own datasets or view state. The OCX owns the host-specific rendering resources. +The control composes every frame in an offscreen DIB-section `FrameBuffer` +and presents it with a single `BitBlt`; `WM_ERASEBKGND` is suppressed because +`OnDraw` paints an opaque frame. Core objects are not thread-safe by default. The control serializes their use. ActiveX-specific implementation units are also compiled into diff --git a/src/MapWinGIS.ActiveX/FrameBuffer.cpp b/src/MapWinGIS.ActiveX/FrameBuffer.cpp new file mode 100644 index 0000000..a476322 --- /dev/null +++ b/src/MapWinGIS.ActiveX/FrameBuffer.cpp @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: MPL-2.0 + +/** + * @file FrameBuffer.cpp + * @brief Implements the offscreen frame buffer. + */ +#include "FrameBuffer.h" + +namespace mwg::activex +{ + namespace + { + [[nodiscard]] HBITMAP CreateBufferBitmap( + const HDC deviceContext, + const int width, + const int height) noexcept + { + BITMAPINFO bitmapInfo{}; + bitmapInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + bitmapInfo.bmiHeader.biWidth = width; + bitmapInfo.bmiHeader.biHeight = -height; // top-down rows + bitmapInfo.bmiHeader.biPlanes = 1; + bitmapInfo.bmiHeader.biBitCount = 32; + bitmapInfo.bmiHeader.biCompression = BI_RGB; + + void* bits = nullptr; + return CreateDIBSection(deviceContext, &bitmapInfo, DIB_RGB_COLORS, &bits, nullptr, 0); + } + } + + FrameBuffer::~FrameBuffer() + { + Release(); + } + + bool FrameBuffer::Resize(const int width, const int height) noexcept + { + if (width <= 0 || height <= 0) + { + return false; + } + + if (IsValid() && width == _width && height == _height) + { + return true; + } + + // Allocate the replacement before releasing the current buffer so a + // failed resize keeps a presentable buffer. + HDC memoryDc = CreateCompatibleDC(nullptr); + if (memoryDc == nullptr) + { + ::OutputDebugStringW(L"MapWinGIS: could not create the frame buffer device context.\n"); + return false; + } + + const HBITMAP bitmap = CreateBufferBitmap(memoryDc, width, height); + if (bitmap == nullptr) + { + ::OutputDebugStringW(L"MapWinGIS: could not allocate the frame buffer bitmap.\n"); + DeleteDC(memoryDc); + return false; + } + + const HBITMAP previousBitmap = static_cast(SelectObject(memoryDc, bitmap)); + if (previousBitmap == nullptr || previousBitmap == HGDI_ERROR) + { + ::OutputDebugStringW(L"MapWinGIS: could not select the frame buffer bitmap.\n"); + DeleteObject(bitmap); + DeleteDC(memoryDc); + return false; + } + + Release(); + _memoryDc = memoryDc; + _bitmap = bitmap; + _previousBitmap = previousBitmap; + _width = width; + _height = height; + return true; + } + + void FrameBuffer::Release() noexcept + { + if (_memoryDc != nullptr) + { + if (_previousBitmap != nullptr && _previousBitmap != HGDI_ERROR) + { + SelectObject(_memoryDc, _previousBitmap); + _previousBitmap = nullptr; + } + DeleteDC(_memoryDc); + _memoryDc = nullptr; + } + + if (_bitmap != nullptr) + { + DeleteObject(_bitmap); + _bitmap = nullptr; + } + + _width = 0; + _height = 0; + } +} diff --git a/src/MapWinGIS.ActiveX/FrameBuffer.h b/src/MapWinGIS.ActiveX/FrameBuffer.h new file mode 100644 index 0000000..4b6b5ec --- /dev/null +++ b/src/MapWinGIS.ActiveX/FrameBuffer.h @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MPL-2.0 + +/** + * @file FrameBuffer.h + * @brief Owns a resizable offscreen device context for flicker-free painting. + * + * Backs a memory device context with a 32-bit DIB section so a complete + * frame can be composed offscreen and presented to the visible surface with + * a single blit. This is host-rendering infrastructure owned by the OCX + * layer; it holds no scene, dataset, or view state. + */ +#pragma once + +#include + +namespace mwg::activex +{ + class FrameBuffer final + { + public: + FrameBuffer() noexcept = default; + ~FrameBuffer(); + + FrameBuffer(const FrameBuffer&) = delete; + FrameBuffer& operator=(const FrameBuffer&) = delete; + + [[nodiscard]] HDC Get() const noexcept { return _memoryDc; } + [[nodiscard]] int Width() const noexcept { return _width; } + [[nodiscard]] int Height() const noexcept { return _height; } + + /// True when Get returns a device context with the bitmap selected. + [[nodiscard]] bool IsValid() const noexcept + { + return _memoryDc != nullptr && _bitmap != nullptr; + } + + /// Recreates the buffer when the size changed and keeps the current + /// bitmap for an equal size. Returns false for non-positive sizes + /// and allocation failures; a failed resize leaves the previous + /// buffer intact so it can still be presented. + [[nodiscard]] bool Resize(int width, int height) noexcept; + + /// Releases the device context and bitmap; the next Resize + /// recreates them. + void Release() noexcept; + + private: + HDC _memoryDc = nullptr; + HBITMAP _bitmap = nullptr; + HBITMAP _previousBitmap = nullptr; + int _width = 0; + int _height = 0; + }; +} diff --git a/src/MapWinGIS.ActiveX/MapWinGIS.ActiveX.Impl.vcxproj b/src/MapWinGIS.ActiveX/MapWinGIS.ActiveX.Impl.vcxproj index 4934b96..d07f85f 100644 --- a/src/MapWinGIS.ActiveX/MapWinGIS.ActiveX.Impl.vcxproj +++ b/src/MapWinGIS.ActiveX/MapWinGIS.ActiveX.Impl.vcxproj @@ -47,6 +47,7 @@ + @@ -57,6 +58,7 @@ + diff --git a/src/MapWinGIS.ActiveX/MapWinGIS.ActiveX.vcxproj b/src/MapWinGIS.ActiveX/MapWinGIS.ActiveX.vcxproj index 7aa2377..be5eefe 100644 --- a/src/MapWinGIS.ActiveX/MapWinGIS.ActiveX.vcxproj +++ b/src/MapWinGIS.ActiveX/MapWinGIS.ActiveX.vcxproj @@ -67,6 +67,7 @@ + diff --git a/tests/MapWinGIS.ActiveX.Tests/FrameBufferTests.cpp b/tests/MapWinGIS.ActiveX.Tests/FrameBufferTests.cpp new file mode 100644 index 0000000..c62c4a0 --- /dev/null +++ b/tests/MapWinGIS.ActiveX.Tests/FrameBufferTests.cpp @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: MPL-2.0 + +/** + * @file FrameBufferTests.cpp + * @brief Tests the offscreen frame buffer used for flicker-free painting. + * + * Covers allocation, resizing, reuse for equal sizes, and pixel round-trips + * through the backed device context. + */ +#include + +#include + +#include "FrameBuffer.h" + +TEST(FrameBuffer, IsInvalidBeforeResize) +{ + mwg::activex::FrameBuffer buffer; + EXPECT_FALSE(buffer.IsValid()); + EXPECT_EQ(buffer.Get(), nullptr); + EXPECT_EQ(buffer.Width(), 0); + EXPECT_EQ(buffer.Height(), 0); +} + +TEST(FrameBuffer, RejectsNonPositiveSizes) +{ + mwg::activex::FrameBuffer buffer; + EXPECT_FALSE(buffer.Resize(0, 10)); + EXPECT_FALSE(buffer.Resize(10, 0)); + EXPECT_FALSE(buffer.Resize(-1, 10)); + EXPECT_FALSE(buffer.Resize(10, -1)); + EXPECT_FALSE(buffer.IsValid()); +} + +TEST(FrameBuffer, AllocatesRequestedSize) +{ + mwg::activex::FrameBuffer buffer; + ASSERT_TRUE(buffer.Resize(120, 80)); + EXPECT_TRUE(buffer.IsValid()); + EXPECT_NE(buffer.Get(), nullptr); + EXPECT_EQ(buffer.Width(), 120); + EXPECT_EQ(buffer.Height(), 80); +} + +TEST(FrameBuffer, KeepsBufferForSameSize) +{ + mwg::activex::FrameBuffer buffer; + ASSERT_TRUE(buffer.Resize(64, 48)); + const HDC deviceContext = buffer.Get(); + + ASSERT_TRUE(buffer.Resize(64, 48)); + EXPECT_EQ(buffer.Get(), deviceContext); + EXPECT_EQ(buffer.Width(), 64); + EXPECT_EQ(buffer.Height(), 48); +} + +TEST(FrameBuffer, ResizesToNewDimensions) +{ + mwg::activex::FrameBuffer buffer; + ASSERT_TRUE(buffer.Resize(64, 48)); + ASSERT_TRUE(buffer.Resize(100, 60)); + EXPECT_TRUE(buffer.IsValid()); + EXPECT_EQ(buffer.Width(), 100); + EXPECT_EQ(buffer.Height(), 60); +} + +TEST(FrameBuffer, FailedResizeKeepsPreviousBuffer) +{ + mwg::activex::FrameBuffer buffer; + ASSERT_TRUE(buffer.Resize(64, 48)); + const HDC deviceContext = buffer.Get(); + + EXPECT_FALSE(buffer.Resize(0, 100)); + EXPECT_TRUE(buffer.IsValid()); + EXPECT_EQ(buffer.Get(), deviceContext); + EXPECT_EQ(buffer.Width(), 64); + EXPECT_EQ(buffer.Height(), 48); +} + +TEST(FrameBuffer, DrawnContentRoundTripsThroughTheBitmap) +{ + mwg::activex::FrameBuffer buffer; + ASSERT_TRUE(buffer.Resize(32, 32)); + + const RECT fill{0, 0, 32, 32}; + ASSERT_NE( + ::FillRect(buffer.Get(), &fill, static_cast(GetStockObject(BLACK_BRUSH))), + 0); + EXPECT_EQ(::GetPixel(buffer.Get(), 10, 10), RGB(0, 0, 0)); + + // A resize replaces the bitmap, so the new buffer starts uninitialized. + ASSERT_TRUE(buffer.Resize(40, 40)); + ASSERT_NE( + ::FillRect(buffer.Get(), &fill, static_cast(GetStockObject(WHITE_BRUSH))), + 0); + EXPECT_EQ(::GetPixel(buffer.Get(), 5, 5), RGB(255, 255, 255)); +} + +TEST(FrameBuffer, ReleaseResetsStateAndAllowsReuse) +{ + mwg::activex::FrameBuffer buffer; + ASSERT_TRUE(buffer.Resize(10, 10)); + + buffer.Release(); + EXPECT_FALSE(buffer.IsValid()); + EXPECT_EQ(buffer.Get(), nullptr); + EXPECT_EQ(buffer.Width(), 0); + EXPECT_EQ(buffer.Height(), 0); + + ASSERT_TRUE(buffer.Resize(10, 10)); + EXPECT_TRUE(buffer.IsValid()); +} diff --git a/tests/MapWinGIS.ActiveX.Tests/MapWinGIS.ActiveX.Tests.vcxproj b/tests/MapWinGIS.ActiveX.Tests/MapWinGIS.ActiveX.Tests.vcxproj index cf2ecc3..c29568e 100644 --- a/tests/MapWinGIS.ActiveX.Tests/MapWinGIS.ActiveX.Tests.vcxproj +++ b/tests/MapWinGIS.ActiveX.Tests/MapWinGIS.ActiveX.Tests.vcxproj @@ -56,6 +56,7 @@ + From bff8d607c081aa008dbc322d9be832fc09f1a562 Mon Sep 17 00:00:00 2001 From: Julian-MW Date: Fri, 6 Nov 2026 20:32:37 +0100 Subject: [PATCH 2/6] =?UTF-8?q?=F0=9F=90=9B=20fix(activex):=20add=20contro?= =?UTF-8?q?l=20frame=20buffer=20declaration=20and=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/MapWinGIS.ActiveX/MapWinGISControl.h | 4 ++++ tests/MapWinGIS.ActiveX.Tests/VectorLayerRendererTests.cpp | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/MapWinGIS.ActiveX/MapWinGISControl.h b/src/MapWinGIS.ActiveX/MapWinGISControl.h index c3ff168..1e02e01 100644 --- a/src/MapWinGIS.ActiveX/MapWinGISControl.h +++ b/src/MapWinGIS.ActiveX/MapWinGISControl.h @@ -19,6 +19,7 @@ #include #include "mapwingis/map/MapView.h" +#include "FrameBuffer.h" #include "GdalEventSource.h" #include "VectorLayerRenderer.h" @@ -98,6 +99,7 @@ class ATL_NO_VTABLE CMapWinGISControl : MESSAGE_HANDLER(WM_CANCELMODE, OnCancelMode) MESSAGE_HANDLER(WM_SIZE, OnSize) MESSAGE_HANDLER(WM_DESTROY, OnDestroy) + MESSAGE_HANDLER(WM_ERASEBKGND, OnEraseBkgnd) CHAIN_MSG_MAP(ATL::CComControl) END_MSG_MAP() @@ -149,6 +151,7 @@ class ATL_NO_VTABLE CMapWinGISControl : LRESULT OnSize(UINT, WPARAM, LPARAM, BOOL& handled); LRESULT OnGdalDispatch(UINT, WPARAM, LPARAM, BOOL& handled); LRESULT OnDestroy(UINT, WPARAM, LPARAM, BOOL& handled); + LRESULT OnEraseBkgnd(UINT, WPARAM, LPARAM, BOOL& handled); static void DispatchGdalNotifications(void* control) noexcept; HRESULT OnDraw(ATL_DRAWINFO& drawInfo); ~CMapWinGISControl(); @@ -166,6 +169,7 @@ class ATL_NO_VTABLE CMapWinGISControl : mwg::MapView _view; LONGLONG _featureCount = 0; + mwg::activex::FrameBuffer _frameBuffer; mwg::activex::VectorLayerRenderer _renderer; mwg::activex::GdalEventSource _gdalEvents; POINT _lastPanPoint{}; diff --git a/tests/MapWinGIS.ActiveX.Tests/VectorLayerRendererTests.cpp b/tests/MapWinGIS.ActiveX.Tests/VectorLayerRendererTests.cpp index f9ceecc..27d3b1f 100644 --- a/tests/MapWinGIS.ActiveX.Tests/VectorLayerRendererTests.cpp +++ b/tests/MapWinGIS.ActiveX.Tests/VectorLayerRendererTests.cpp @@ -289,7 +289,8 @@ TEST(VectorLayerRenderer, OpenGlRejectsTheFrameWhenAnyLayerIsUnsupported) S_FALSE); EXPECT_EQ( renderer.GetLastFailureReason(), - L"The scene uses geometry that the OpenGL backend does not support."); + L"The OpenGL preview backend supports only convex, hole-free " + L"polygons; this frame was rendered with GDI."); } TEST(VectorLayerRenderer, FailsWithoutDrawingNonFiniteCoordinates) @@ -359,7 +360,8 @@ TEST(VectorLayerRenderer, UsesGdiForPolygonWithInteriorRing) mwg::activex::VectorRenderBackendKind::Gdi); EXPECT_EQ( renderer.GetDiagnostic(), - L"The scene uses geometry that the OpenGL backend does not support."); + L"The OpenGL preview backend supports only convex, hole-free " + L"polygons; this frame was rendered with GDI."); EXPECT_EQ(GetPixel(surface.Get(), 30, 50), kFill); EXPECT_EQ(GetPixel(surface.Get(), 60, 50), GetSysColor(COLOR_WINDOW)); } From 3fcf7e1d259a70edaeb0dca010fcf89b94a9fade Mon Sep 17 00:00:00 2001 From: Julian-MW Date: Fri, 6 Nov 2026 20:34:52 +0100 Subject: [PATCH 3/6] =?UTF-8?q?=F0=9F=90=9B=20fix(activex):=20reword=20the?= =?UTF-8?q?=20OpenGL=20fallback=20diagnostic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/MapWinGIS.ActiveX/OpenGlVectorLayerRenderer.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/MapWinGIS.ActiveX/OpenGlVectorLayerRenderer.cpp b/src/MapWinGIS.ActiveX/OpenGlVectorLayerRenderer.cpp index 4a19365..1ae91f1 100644 --- a/src/MapWinGIS.ActiveX/OpenGlVectorLayerRenderer.cpp +++ b/src/MapWinGIS.ActiveX/OpenGlVectorLayerRenderer.cpp @@ -564,7 +564,8 @@ namespace mwg::activex if (!IsSupported(scene)) { _lastFailureReason = - L"The scene uses geometry that the OpenGL backend does not support."; + L"The OpenGL preview backend supports only convex, hole-free " + L"polygons; this frame was rendered with GDI."; return {S_FALSE, std::nullopt}; } From 6b676346e22c0838af8bd63b26e4f3efbd072a9b Mon Sep 17 00:00:00 2001 From: Julian-MW Date: Fri, 6 Nov 2026 20:36:19 +0100 Subject: [PATCH 4/6] =?UTF-8?q?=F0=9F=90=9B=20fix(activex):=20compose=20co?= =?UTF-8?q?ntrol=20frames=20offscreen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/MapWinGIS.ActiveX/MapWinGISControl.cpp | 570 +-------------------- 1 file changed, 5 insertions(+), 565 deletions(-) diff --git a/src/MapWinGIS.ActiveX/MapWinGISControl.cpp b/src/MapWinGIS.ActiveX/MapWinGISControl.cpp index 79507dd..01823f7 100644 --- a/src/MapWinGIS.ActiveX/MapWinGISControl.cpp +++ b/src/MapWinGIS.ActiveX/MapWinGISControl.cpp @@ -7,8 +7,10 @@ * * Redraws: every map or view change made through the control goes through * its MapView; the control calls FireViewChange when the MapView reports - * that the frame changed. OnDraw draws the MapView's render scene through - * its viewport, so every layer uses the same transformation. + * that the frame changed. OnDraw composes the MapView's render scene through + * its viewport into an offscreen frame buffer and presents the finished + * frame with a single blit, so every layer uses the same transformation and + * the visible surface never shows a partially drawn frame. */ #include "MapWinGISControl.h" @@ -524,567 +526,5 @@ STDMETHODIMP CMapWinGISControl::get_RenderingBackend(BSTR* backend) STDMETHODIMP CMapWinGISControl::get_RenderingStatus(BSTR* status) { - return CopyToBstr(_renderer.GetDiagnostic(), status); -} - -STDMETHODIMP CMapWinGISControl::get_RenderingBackendPreference(BSTR* preference) -{ - return CopyToBstr(PreferenceName(_renderer.GetPreference()), preference); -} - -STDMETHODIMP CMapWinGISControl::put_RenderingBackendPreference(BSTR preference) -{ - if (preference == nullptr) - { - return E_INVALIDARG; - } - - const UINT length = ::SysStringLen(preference); - auto equals = [preference, length](const wchar_t* value, const int valueLength) - { - return length == static_cast(valueLength) && - ::CompareStringOrdinal( - preference, - static_cast(length), - value, - valueLength, - TRUE) == CSTR_EQUAL; - }; - - if (equals(L"Automatic", 9)) - { - _renderer.SetPreference(mwg::activex::VectorRenderBackendPreference::Automatic); - } - else if (equals(L"OpenGL", 6)) - { - _renderer.SetPreference(mwg::activex::VectorRenderBackendPreference::OpenGL); - } - else if (equals(L"GDI", 3)) - { - _renderer.SetPreference(mwg::activex::VectorRenderBackendPreference::Gdi); - } - else - { - return E_INVALIDARG; - } - - FireViewChange(); - return S_OK; -} - -STDMETHODIMP CMapWinGISControl::FitToMap() -{ - bool changed = false; - const HRESULT result = RunViewOperation( - [this]() { return _view.FitToMap(); }, - changed); - if (SUCCEEDED(result) && changed) - { - FireViewChange(); - } - return result; -} - -STDMETHODIMP CMapWinGISControl::ZoomIn(const DOUBLE factor) -{ - bool changed = false; - const HRESULT result = RunViewOperation( - [this, factor]() { return _view.ZoomIn(factor); }, - changed); - if (SUCCEEDED(result) && changed) - { - FireViewChange(); - } - return result; -} - -STDMETHODIMP CMapWinGISControl::ZoomOut(const DOUBLE factor) -{ - bool changed = false; - const HRESULT result = RunViewOperation( - [this, factor]() { return _view.ZoomOut(factor); }, - changed); - if (SUCCEEDED(result) && changed) - { - FireViewChange(); - } - return result; -} - -STDMETHODIMP CMapWinGISControl::Pan( - const DOUBLE deltaX, - const DOUBLE deltaY) -{ - bool changed = false; - const HRESULT result = RunViewOperation( - [this, deltaX, deltaY]() { return _view.Pan(deltaX, deltaY); }, - changed); - if (SUCCEEDED(result) && changed) - { - FireViewChange(); - } - return result; -} - -STDMETHODIMP CMapWinGISControl::ZoomAt( - const DOUBLE x, - const DOUBLE y, - const DOUBLE factor) -{ - bool changed = false; - const HRESULT result = RunViewOperation( - [this, x, y, factor]() - { - return _view.ZoomAt({x, y}, factor); - }, - changed); - if (SUCCEEDED(result) && changed) - { - FireViewChange(); - } - return result; -} - -STDMETHODIMP CMapWinGISControl::ZoomToLayer(const LONGLONG layerId) -{ - if (layerId <= 0) - { - return SetControlComErrorInfo("Layer ID must be positive", IID_IMapWinGISControl4, E_INVALIDARG); - } - const mwg::LayerId id{static_cast(layerId)}; - if (_view.GetMap().FindLayer(id) == nullptr) - { - return SetControlComErrorInfo("Layer ID was not found", IID_IMapWinGISControl4, E_INVALIDARG); - } - - bool changed = false; - const HRESULT result = RunViewOperation( - [this, id]() { return _view.ZoomToLayer(id); }, - changed); - if (SUCCEEDED(result) && changed) - { - FireViewChange(); - } - return result; -} - -STDMETHODIMP CMapWinGISControl::get_LayerCount(LONG* count) -{ - if (count == nullptr) - { - return E_POINTER; - } - *count = _view.GetMap().LayerCount(); - return S_OK; -} - -STDMETHODIMP CMapWinGISControl::GetLayerId( - const LONG index, - LONGLONG* layerId) -{ - if (layerId == nullptr) - { - return E_POINTER; - } - *layerId = 0; - const mwg::Map& map = _view.GetMap(); - if (index < 0 || index >= map.LayerCount()) - { - return E_INVALIDARG; - } - - return CopyLayerId(map.Layer(index).Id(), layerId); -} - -bool CMapWinGISControl::ResizeView(const LONG width, const LONG height) noexcept -{ - try - { - return _view.Resize( - static_cast(std::clamp(width, 0, mwg::Viewport::kMaxDimension)), - static_cast(std::clamp(height, 0, mwg::Viewport::kMaxDimension))); - } - catch (const std::exception& exception) - { - ::OutputDebugStringA(exception.what()); - return false; - } -} - -bool CMapWinGISControl::CaptureMouse() noexcept -{ - if (m_bWndLess) - { - return m_spInPlaceSite != nullptr && - m_spInPlaceSite->SetCapture(TRUE) == S_OK; - } - - if (m_hWnd == nullptr) - { - return false; - } - - ::SetCapture(m_hWnd); - return ::GetCapture() == m_hWnd; -} - -void CMapWinGISControl::ReleaseMouseCapture() noexcept -{ - if (m_bWndLess) - { - if (m_spInPlaceSite != nullptr) - { - m_spInPlaceSite->SetCapture(FALSE); - } - } - else if (m_hWnd != nullptr && ::GetCapture() == m_hWnd) - { - ::ReleaseCapture(); - } -} - -void CMapWinGISControl::CancelMousePan() noexcept -{ - if (_isPanning) - { - _isPanning = false; - ReleaseMouseCapture(); - } -} - -bool CMapWinGISControl::GetMousePoint( - const LPARAM lParam, - const bool screenCoordinates, - POINT& point) const noexcept -{ - point = POINT{GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)}; - - HWND coordinateWindow = m_hWnd; - if (screenCoordinates) - { - if (m_bWndLess) - { - if (m_spInPlaceSite == nullptr || - FAILED(m_spInPlaceSite->GetWindow(&coordinateWindow))) - { - return false; - } - } - - if (coordinateWindow == nullptr || - !::ScreenToClient(coordinateWindow, &point)) - { - return false; - } - } - - if (m_bWndLess) - { - point.x -= m_rcPos.left; - point.y -= m_rcPos.top; - } - return true; -} - -LRESULT CMapWinGISControl::OnLButtonDown( - UINT, - WPARAM, - const LPARAM lParam, - BOOL& handled) -{ - POINT point{}; - if (!GetMousePoint(lParam, false, point) || !CaptureMouse()) - { - handled = FALSE; - return 0; - } - - _lastPanPoint = point; - _isPanning = true; - handled = TRUE; - return 0; -} - -LRESULT CMapWinGISControl::OnMouseMove( - UINT, - const WPARAM wParam, - const LPARAM lParam, - BOOL& handled) -{ - if (!_isPanning) - { - handled = FALSE; - return 0; - } - - if ((wParam & MK_LBUTTON) == 0) - { - CancelMousePan(); - handled = TRUE; - return 0; - } - - POINT point{}; - if (!GetMousePoint(lParam, false, point)) - { - handled = TRUE; - return 0; - } - - const LONG pixelDeltaX = point.x - _lastPanPoint.x; - const LONG pixelDeltaY = point.y - _lastPanPoint.y; - _lastPanPoint = point; - if (pixelDeltaX == 0 && pixelDeltaY == 0) - { - handled = TRUE; - return 0; - } - - const std::optional resolution = _view.GetViewport().Resolution(); - if (!resolution.has_value()) - { - handled = TRUE; - return 0; - } - - bool changed = false; - const HRESULT result = RunViewOperation( - [this, pixelDeltaX, pixelDeltaY, resolution]() - { - return _view.Pan( - -static_cast(pixelDeltaX) * *resolution, - static_cast(pixelDeltaY) * *resolution); - }, - changed); - if (SUCCEEDED(result) && changed) - { - FireViewChange(); - } - - handled = TRUE; - return 0; -} - -LRESULT CMapWinGISControl::OnLButtonUp( - UINT, - WPARAM, - LPARAM, - BOOL& handled) -{ - if (!_isPanning) - { - handled = FALSE; - return 0; - } - - CancelMousePan(); - handled = TRUE; - return 0; -} - -LRESULT CMapWinGISControl::OnMouseWheel( - UINT, - const WPARAM wParam, - const LPARAM lParam, - BOOL& handled) -{ - const int wheelDelta = GET_WHEEL_DELTA_WPARAM(wParam); - POINT point{}; - const mwg::Viewport& viewport = _view.GetViewport(); - if (wheelDelta == 0 || - !viewport.IsValid() || - !GetMousePoint(lParam, true, point) || - point.x < 0 || point.x >= viewport.Width() || - point.y < 0 || point.y >= viewport.Height()) - { - handled = FALSE; - return 0; - } - - const double factor = std::pow( - kMouseWheelZoomFactor, - static_cast(wheelDelta) / WHEEL_DELTA); - bool changed = false; - const HRESULT result = RunViewOperation( - [this, point, factor]() - { - return _view.ZoomAt( - {static_cast(point.x), static_cast(point.y)}, - factor); - }, - changed); - if (SUCCEEDED(result) && changed) - { - FireViewChange(); - } - - handled = TRUE; - return 0; -} - -LRESULT CMapWinGISControl::OnCaptureChanged( - UINT, - WPARAM, - LPARAM, - BOOL& handled) -{ - _isPanning = false; - handled = TRUE; - return 0; -} - -LRESULT CMapWinGISControl::OnCancelMode( - UINT, - WPARAM, - LPARAM, - BOOL& handled) -{ - CancelMousePan(); - handled = TRUE; - return 0; -} - -LRESULT CMapWinGISControl::OnSize( - UINT, - WPARAM, - const LPARAM lParam, - BOOL& handled) -{ - _gdalEvents.OnSize(); - if (ResizeView(LOWORD(lParam), HIWORD(lParam))) - { - FireViewChange(); - } - handled = TRUE; - return 0; -} - -LRESULT CMapWinGISControl::OnGdalDispatch( - UINT, - WPARAM, - LPARAM, - BOOL& handled) -{ - return _gdalEvents.OnDispatch(handled); -} - -LRESULT CMapWinGISControl::OnDestroy( - UINT, - WPARAM, - LPARAM, - BOOL& handled) -{ - CancelMousePan(); - _gdalEvents.OnDestroy(); - handled = FALSE; - return 0; -} - -void CMapWinGISControl::DispatchGdalNotifications(void* const control) noexcept -{ - auto* const instance = static_cast(control); - ATL::CComPtr lifetime(instance); - BOOL handled = FALSE; - try - { - instance->OnGdalDispatch( - mwg::activex::kGdalDispatchMessage, - 0, - 0, - handled); - } - catch (const std::exception& exception) - { - ::OutputDebugStringA(exception.what()); - } -} - -STDMETHODIMP CMapWinGISControl::Close(const DWORD saveOption) -{ - CancelMousePan(); - _gdalEvents.Close(); - return ATL::IOleObjectImpl::Close(saveOption); -} - -STDMETHODIMP CMapWinGISControl::SetObjectRects(LPCRECT position, LPCRECT clip) -{ - if (position == nullptr || clip == nullptr) - { - return E_POINTER; - } - const HRESULT result = - ATL::IOleInPlaceObjectWindowlessImpl::SetObjectRects(position, clip); - if (SUCCEEDED(result) && m_bWndLess) - { - const LONGLONG width = static_cast(position->right) - position->left; - const LONGLONG height = static_cast(position->bottom) - position->top; - if (ResizeView( - static_cast(std::clamp(width, 0, mwg::Viewport::kMaxDimension)), - static_cast(std::clamp(height, 0, mwg::Viewport::kMaxDimension)))) - { - FireViewChange(); - } - } - return result; -} - -HRESULT CMapWinGISControl::OnDraw(ATL_DRAWINFO& drawInfo) -{ - if (drawInfo.hdcDraw == nullptr || drawInfo.prcBounds == nullptr) - { - return E_POINTER; - } - - const RECT bounds{ - drawInfo.prcBounds->left, - drawInfo.prcBounds->top, - drawInfo.prcBounds->right, - drawInfo.prcBounds->bottom}; - - if (::FillRect( - drawInfo.hdcDraw, - &bounds, - ::GetSysColorBrush(COLOR_WINDOW)) == 0) - { - return E_FAIL; - } - - const LONGLONG width = static_cast(bounds.right) - bounds.left; - const LONGLONG height = static_cast(bounds.bottom) - bounds.top; - if (width <= 0 || height <= 0 || - width > mwg::Viewport::kMaxDimension || height > mwg::Viewport::kMaxDimension) - { - return S_OK; - } - try - { - const mwg::rendering::MapRenderScene& scene = _view.Scene(); - // Printing and scaled draws use a snapshot, never the live viewport. - mwg::Viewport viewport = _view.GetViewport(); - viewport.SetSize(static_cast(width), static_cast(height)); - if (!viewport.HasView()) - { - (void)viewport.FitExtent(_view.GetMap().GetExtent(), mwg::MapView::kDefaultPadding); - } - if (scene.layers.empty() || !viewport.IsValid()) - { - return S_OK; - } - return _renderer.Draw(drawInfo.hdcDraw, bounds, scene, viewport); - } - catch (const std::bad_alloc&) - { - return E_OUTOFMEMORY; - } - catch (const std::exception& exception) - { - ::OutputDebugStringA(exception.what()); - return E_FAIL; - } -} - -CMapWinGISControl::~CMapWinGISControl() -{ - CancelMousePan(); - _gdalEvents.Shutdown(); + return CopyToBstr(_renderer.GetDiagnostic(), backend); } From 15255ab8f9739b7bb8eca522c9d96e852e45f925 Mon Sep 17 00:00:00 2001 From: Julian-MW Date: Fri, 6 Nov 2026 20:39:19 +0100 Subject: [PATCH 5/6] =?UTF-8?q?=F0=9F=90=9B=20fix(activex):=20complete=20t?= =?UTF-8?q?he=20offscreen=20frame=20composition=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/MapWinGIS.ActiveX/MapWinGISControl.cpp | 608 ++++++++++++++++++++- 1 file changed, 607 insertions(+), 1 deletion(-) diff --git a/src/MapWinGIS.ActiveX/MapWinGISControl.cpp b/src/MapWinGIS.ActiveX/MapWinGISControl.cpp index 01823f7..9ca7d35 100644 --- a/src/MapWinGIS.ActiveX/MapWinGISControl.cpp +++ b/src/MapWinGIS.ActiveX/MapWinGISControl.cpp @@ -526,5 +526,611 @@ STDMETHODIMP CMapWinGISControl::get_RenderingBackend(BSTR* backend) STDMETHODIMP CMapWinGISControl::get_RenderingStatus(BSTR* status) { - return CopyToBstr(_renderer.GetDiagnostic(), backend); + return CopyToBstr(_renderer.GetDiagnostic(), status); +} + +STDMETHODIMP CMapWinGISControl::get_RenderingBackendPreference(BSTR* preference) +{ + return CopyToBstr(PreferenceName(_renderer.GetPreference()), preference); +} + +STDMETHODIMP CMapWinGISControl::put_RenderingBackendPreference(BSTR preference) +{ + if (preference == nullptr) + { + return E_INVALIDARG; + } + + const UINT length = ::SysStringLen(preference); + auto equals = [preference, length](const wchar_t* value, const int valueLength) + { + return length == static_cast(valueLength) && + ::CompareStringOrdinal( + preference, + static_cast(length), + value, + valueLength, + TRUE) == CSTR_EQUAL; + }; + + if (equals(L"Automatic", 9)) + { + _renderer.SetPreference(mwg::activex::VectorRenderBackendPreference::Automatic); + } + else if (equals(L"OpenGL", 6)) + { + _renderer.SetPreference(mwg::activex::VectorRenderBackendPreference::OpenGL); + } + else if (equals(L"GDI", 3)) + { + _renderer.SetPreference(mwg::activex::VectorRenderBackendPreference::Gdi); + } + else + { + return E_INVALIDARG; + } + + FireViewChange(); + return S_OK; +} + +STDMETHODIMP CMapWinGISControl::FitToMap() +{ + bool changed = false; + const HRESULT result = RunViewOperation( + [this]() { return _view.FitToMap(); }, + changed); + if (SUCCEEDED(result) && changed) + { + FireViewChange(); + } + return result; +} + +STDMETHODIMP CMapWinGISControl::ZoomIn(const DOUBLE factor) +{ + bool changed = false; + const HRESULT result = RunViewOperation( + [this, factor]() { return _view.ZoomIn(factor); }, + changed); + if (SUCCEEDED(result) && changed) + { + FireViewChange(); + } + return result; +} + +STDMETHODIMP CMapWinGISControl::ZoomOut(const DOUBLE factor) +{ + bool changed = false; + const HRESULT result = RunViewOperation( + [this, factor]() { return _view.ZoomOut(factor); }, + changed); + if (SUCCEEDED(result) && changed) + { + FireViewChange(); + } + return result; +} + +STDMETHODIMP CMapWinGISControl::Pan( + const DOUBLE deltaX, + const DOUBLE deltaY) +{ + bool changed = false; + const HRESULT result = RunViewOperation( + [this, deltaX, deltaY]() { return _view.Pan(deltaX, deltaY); }, + changed); + if (SUCCEEDED(result) && changed) + { + FireViewChange(); + } + return result; +} + +STDMETHODIMP CMapWinGISControl::ZoomAt( + const DOUBLE x, + const DOUBLE y, + const DOUBLE factor) +{ + bool changed = false; + const HRESULT result = RunViewOperation( + [this, x, y, factor]() + { + return _view.ZoomAt({x, y}, factor); + }, + changed); + if (SUCCEEDED(result) && changed) + { + FireViewChange(); + } + return result; +} + +STDMETHODIMP CMapWinGISControl::ZoomToLayer(const LONGLONG layerId) +{ + if (layerId <= 0) + { + return SetControlComErrorInfo("Layer ID must be positive", IID_IMapWinGISControl4, E_INVALIDARG); + } + const mwg::LayerId id{static_cast(layerId)}; + if (_view.GetMap().FindLayer(id) == nullptr) + { + return SetControlComErrorInfo("Layer ID was not found", IID_IMapWinGISControl4, E_INVALIDARG); + } + + bool changed = false; + const HRESULT result = RunViewOperation( + [this, id]() { return _view.ZoomToLayer(id); }, + changed); + if (SUCCEEDED(result) && changed) + { + FireViewChange(); + } + return result; +} + +STDMETHODIMP CMapWinGISControl::get_LayerCount(LONG* count) +{ + if (count == nullptr) + { + return E_POINTER; + } + *count = _view.GetMap().LayerCount(); + return S_OK; +} + +STDMETHODIMP CMapWinGISControl::GetLayerId( + const LONG index, + LONGLONG* layerId) +{ + if (layerId == nullptr) + { + return E_POINTER; + } + *layerId = 0; + const mwg::Map& map = _view.GetMap(); + if (index < 0 || index >= map.LayerCount()) + { + return E_INVALIDARG; + } + + return CopyLayerId(map.Layer(index).Id(), layerId); +} + +bool CMapWinGISControl::ResizeView(const LONG width, const LONG height) noexcept +{ + try + { + return _view.Resize( + static_cast(std::clamp(width, 0, mwg::Viewport::kMaxDimension)), + static_cast(std::clamp(height, 0, mwg::Viewport::kMaxDimension))); + } + catch (const std::exception& exception) + { + ::OutputDebugStringA(exception.what()); + return false; + } +} + +bool CMapWinGISControl::CaptureMouse() noexcept +{ + if (m_bWndLess) + { + return m_spInPlaceSite != nullptr && + m_spInPlaceSite->SetCapture(TRUE) == S_OK; + } + + if (m_hWnd == nullptr) + { + return false; + } + + ::SetCapture(m_hWnd); + return ::GetCapture() == m_hWnd; +} + +void CMapWinGISControl::ReleaseMouseCapture() noexcept +{ + if (m_bWndLess) + { + if (m_spInPlaceSite != nullptr) + { + m_spInPlaceSite->SetCapture(FALSE); + } + } + else if (m_hWnd != nullptr && ::GetCapture() == m_hWnd) + { + ::ReleaseCapture(); + } +} + +void CMapWinGISControl::CancelMousePan() noexcept +{ + if (_isPanning) + { + _isPanning = false; + ReleaseMouseCapture(); + } +} + +bool CMapWinGISControl::GetMousePoint( + const LPARAM lParam, + const bool screenCoordinates, + POINT& point) const noexcept +{ + point = POINT{GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)}; + + HWND coordinateWindow = m_hWnd; + if (screenCoordinates) + { + if (m_bWndLess) + { + if (m_spInPlaceSite == nullptr || + FAILED(m_spInPlaceSite->GetWindow(&coordinateWindow))) + { + return false; + } + } + + if (coordinateWindow == nullptr || + !::ScreenToClient(coordinateWindow, &point)) + { + return false; + } + } + + if (m_bWndLess) + { + point.x -= m_rcPos.left; + point.y -= m_rcPos.top; + } + return true; +} + +LRESULT CMapWinGISControl::OnLButtonDown( + UINT, + WPARAM, + const LPARAM lParam, + BOOL& handled) +{ + POINT point{}; + if (!GetMousePoint(lParam, false, point) || !CaptureMouse()) + { + handled = FALSE; + return 0; + } + + _lastPanPoint = point; + _isPanning = true; + handled = TRUE; + return 0; +} + +LRESULT CMapWinGISControl::OnMouseMove( + UINT, + const WPARAM wParam, + const LPARAM lParam, + BOOL& handled) +{ + if (!_isPanning) + { + handled = FALSE; + return 0; + } + + if ((wParam & MK_LBUTTON) == 0) + { + CancelMousePan(); + handled = TRUE; + return 0; + } + + POINT point{}; + if (!GetMousePoint(lParam, false, point)) + { + handled = TRUE; + return 0; + } + + const LONG pixelDeltaX = point.x - _lastPanPoint.x; + const LONG pixelDeltaY = point.y - _lastPanPoint.y; + _lastPanPoint = point; + if (pixelDeltaX == 0 && pixelDeltaY == 0) + { + handled = TRUE; + return 0; + } + + const std::optional resolution = _view.GetViewport().Resolution(); + if (!resolution.has_value()) + { + handled = TRUE; + return 0; + } + + bool changed = false; + const HRESULT result = RunViewOperation( + [this, pixelDeltaX, pixelDeltaY, resolution]() + { + return _view.Pan( + -static_cast(pixelDeltaX) * *resolution, + static_cast(pixelDeltaY) * *resolution); + }, + changed); + if (SUCCEEDED(result) && changed) + { + FireViewChange(); + } + + handled = TRUE; + return 0; +} + +LRESULT CMapWinGISControl::OnLButtonUp( + UINT, + WPARAM, + LPARAM, + BOOL& handled) +{ + if (!_isPanning) + { + handled = FALSE; + return 0; + } + + CancelMousePan(); + handled = TRUE; + return 0; +} + +LRESULT CMapWinGISControl::OnMouseWheel( + UINT, + const WPARAM wParam, + const LPARAM lParam, + BOOL& handled) +{ + const int wheelDelta = GET_WHEEL_DELTA_WPARAM(wParam); + POINT point{}; + const mwg::Viewport& viewport = _view.GetViewport(); + if (wheelDelta == 0 || + !viewport.IsValid() || + !GetMousePoint(lParam, true, point) || + point.x < 0 || point.x >= viewport.Width() || + point.y < 0 || point.y >= viewport.Height()) + { + handled = FALSE; + return 0; + } + + const double factor = std::pow( + kMouseWheelZoomFactor, + static_cast(wheelDelta) / WHEEL_DELTA); + bool changed = false; + const HRESULT result = RunViewOperation( + [this, point, factor]() + { + return _view.ZoomAt( + {static_cast(point.x), static_cast(point.y)}, + factor); + }, + changed); + if (SUCCEEDED(result) && changed) + { + FireViewChange(); + } + + handled = TRUE; + return 0; +} + +LRESULT CMapWinGISControl::OnCaptureChanged( + UINT, + WPARAM, + LPARAM, + BOOL& handled) +{ + _isPanning = false; + handled = TRUE; + return 0; +} + +LRESULT CMapWinGISControl::OnCancelMode( + UINT, + WPARAM, + LPARAM, + BOOL& handled) +{ + CancelMousePan(); + handled = TRUE; + return 0; +} + +LRESULT CMapWinGISControl::OnSize( + UINT, + WPARAM, + const LPARAM lParam, + BOOL& handled) +{ + _gdalEvents.OnSize(); + if (ResizeView(LOWORD(lParam), HIWORD(lParam))) + { + FireViewChange(); + } + handled = TRUE; + return 0; +} + +LRESULT CMapWinGISControl::OnGdalDispatch( + UINT, + WPARAM, + LPARAM, + BOOL& handled) +{ + return _gdalEvents.OnDispatch(handled); +} + +LRESULT CMapWinGISControl::OnDestroy( + UINT, + WPARAM, + LPARAM, + BOOL& handled) +{ + CancelMousePan(); + _gdalEvents.OnDestroy(); + handled = FALSE; + return 0; +} + +void CMapWinGISControl::DispatchGdalNotifications(void* const control) noexcept +{ + auto* const instance = static_cast(control); + ATL::CComPtr lifetime(instance); + BOOL handled = FALSE; + try + { + instance->OnGdalDispatch( + mwg::activex::kGdalDispatchMessage, + 0, + 0, + handled); + } + catch (const std::exception& exception) + { + ::OutputDebugStringA(exception.what()); + } +} + +STDMETHODIMP CMapWinGISControl::Close(const DWORD saveOption) +{ + CancelMousePan(); + _gdalEvents.Close(); + return ATL::IOleObjectImpl::Close(saveOption); +} + +STDMETHODIMP CMapWinGISControl::SetObjectRects(LPCRECT position, LPCRECT clip) +{ + if (position == nullptr || clip == nullptr) + { + return E_POINTER; + } + const HRESULT result = + ATL::IOleInPlaceObjectWindowlessImpl::SetObjectRects(position, clip); + if (SUCCEEDED(result) && m_bWndLess) + { + const LONGLONG width = static_cast(position->right) - position->left; + const LONGLONG height = static_cast(position->bottom) - position->top; + if (ResizeView( + static_cast(std::clamp(width, 0, mwg::Viewport::kMaxDimension)), + static_cast(std::clamp(height, 0, mwg::Viewport::kMaxDimension)))) + { + FireViewChange(); + } + } + return result; +} + +HRESULT CMapWinGISControl::OnDraw(ATL_DRAWINFO& drawInfo) +{ + if (drawInfo.hdcDraw == nullptr || drawInfo.prcBounds == nullptr) + { + return E_POINTER; + } + + const RECT bounds{ + drawInfo.prcBounds->left, + drawInfo.prcBounds->top, + drawInfo.prcBounds->right, + drawInfo.prcBounds->bottom}; + + const LONGLONG width = static_cast(bounds.right) - bounds.left; + const LONGLONG height = static_cast(bounds.bottom) - bounds.top; + const bool drawable = width > 0 && height > 0 && + width <= mwg::Viewport::kMaxDimension && height <= mwg::Viewport::kMaxDimension; + + // Compose the frame in an offscreen buffer and present it with a single + // blit, so the visible surface never shows the background erase or a + // partially drawn frame. Drawing straight to the target device context + // is the fallback for sizes the buffer cannot hold. + const bool buffered = drawable && + _frameBuffer.Resize( + static_cast(width), + static_cast(height)); + HDC frameDc = buffered ? _frameBuffer.Get() : drawInfo.hdcDraw; + RECT frameBounds = buffered + ? RECT{0, 0, static_cast(width), static_cast(height)} + : bounds; + + if (::FillRect( + frameDc, + &frameBounds, + ::GetSysColorBrush(COLOR_WINDOW)) == 0) + { + return E_FAIL; + } + + if (!drawable) + { + return S_OK; + } + + HRESULT result = S_OK; + try + { + const mwg::rendering::MapRenderScene& scene = _view.Scene(); + // Printing and scaled draws use a snapshot, never the live viewport. + mwg::Viewport viewport = _view.GetViewport(); + viewport.SetSize(static_cast(width), static_cast(height)); + if (!viewport.HasView()) + { + (void)viewport.FitExtent(_view.GetMap().GetExtent(), mwg::MapView::kDefaultPadding); + } + if (!scene.layers.empty() && viewport.IsValid()) + { + result = _renderer.Draw(frameDc, frameBounds, scene, viewport); + } + } + catch (const std::bad_alloc&) + { + result = E_OUTOFMEMORY; + } + catch (const std::exception& exception) + { + ::OutputDebugStringA(exception.what()); + result = E_FAIL; + } + + if (buffered && ::BitBlt( + drawInfo.hdcDraw, + bounds.left, + bounds.top, + static_cast(width), + static_cast(height), + frameDc, + 0, + 0, + SRCCOPY) == 0) + { + return E_FAIL; + } + + return result; +} + +LRESULT CMapWinGISControl::OnEraseBkgnd( + UINT, + WPARAM, + LPARAM, + BOOL& handled) +{ + // OnDraw paints an opaque frame over the whole bounds, so erasing the + // background first would only flash the host's color through. + handled = TRUE; + return 1; +} + +CMapWinGISControl::~CMapWinGISControl() +{ + CancelMousePan(); + _gdalEvents.Shutdown(); } From 92b9a2551a1a479d662f5835af99ba60e54f5cfe Mon Sep 17 00:00:00 2001 From: Julian-MW Date: Fri, 6 Nov 2026 22:56:03 +0100 Subject: [PATCH 6/6] =?UTF-8?q?=F0=9F=90=9B=20fix(activex):=20fall=20back?= =?UTF-8?q?=20to=20direct=20rendering=20when=20the=20blit=20fails?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Compose the frame through a renderFrame lambda used for both the buffered and direct paths - On failure of BitBlt from the offscreen memory DC (cross-device targets such as printers or metafiles), redraw the frame directly to drawInfo.hdcDraw instead of returning E_FAIL --- src/MapWinGIS.ActiveX/MapWinGISControl.cpp | 102 ++++++++++++--------- 1 file changed, 59 insertions(+), 43 deletions(-) diff --git a/src/MapWinGIS.ActiveX/MapWinGISControl.cpp b/src/MapWinGIS.ActiveX/MapWinGISControl.cpp index 9ca7d35..7fc8d3a 100644 --- a/src/MapWinGIS.ActiveX/MapWinGISControl.cpp +++ b/src/MapWinGIS.ActiveX/MapWinGISControl.cpp @@ -1061,60 +1061,76 @@ HRESULT CMapWinGISControl::OnDraw(ATL_DRAWINFO& drawInfo) ? RECT{0, 0, static_cast(width), static_cast(height)} : bounds; - if (::FillRect( - frameDc, - &frameBounds, - ::GetSysColorBrush(COLOR_WINDOW)) == 0) + const auto renderFrame = [&]() -> HRESULT { - return E_FAIL; - } + if (::FillRect( + frameDc, + &frameBounds, + ::GetSysColorBrush(COLOR_WINDOW)) == 0) + { + return E_FAIL; + } - if (!drawable) - { - return S_OK; - } + if (!drawable) + { + return S_OK; + } - HRESULT result = S_OK; - try - { - const mwg::rendering::MapRenderScene& scene = _view.Scene(); - // Printing and scaled draws use a snapshot, never the live viewport. - mwg::Viewport viewport = _view.GetViewport(); - viewport.SetSize(static_cast(width), static_cast(height)); - if (!viewport.HasView()) + try { - (void)viewport.FitExtent(_view.GetMap().GetExtent(), mwg::MapView::kDefaultPadding); + const mwg::rendering::MapRenderScene& scene = _view.Scene(); + // Printing and scaled draws use a snapshot, never the live viewport. + mwg::Viewport viewport = _view.GetViewport(); + viewport.SetSize(static_cast(width), static_cast(height)); + if (!viewport.HasView()) + { + (void)viewport.FitExtent(_view.GetMap().GetExtent(), mwg::MapView::kDefaultPadding); + } + if (!scene.layers.empty() && viewport.IsValid()) + { + return _renderer.Draw(frameDc, frameBounds, scene, viewport); + } + return S_OK; } - if (!scene.layers.empty() && viewport.IsValid()) + catch (const std::bad_alloc&) { - result = _renderer.Draw(frameDc, frameBounds, scene, viewport); + return E_OUTOFMEMORY; } - } - catch (const std::bad_alloc&) - { - result = E_OUTOFMEMORY; - } - catch (const std::exception& exception) - { - ::OutputDebugStringA(exception.what()); - result = E_FAIL; - } + catch (const std::exception& exception) + { + ::OutputDebugStringA(exception.what()); + return E_FAIL; + } + }; - if (buffered && ::BitBlt( - drawInfo.hdcDraw, - bounds.left, - bounds.top, - static_cast(width), - static_cast(height), - frameDc, - 0, - 0, - SRCCOPY) == 0) + if (buffered) { - return E_FAIL; + const HRESULT result = renderFrame(); + if (FAILED(result)) + { + return result; + } + if (::BitBlt( + drawInfo.hdcDraw, + bounds.left, + bounds.top, + static_cast(width), + static_cast(height), + frameDc, + 0, + 0, + SRCCOPY) != 0) + { + return result; + } + + // A blit between different devices (printer or metafile targets) + // can fail; present the frame by drawing straight to the target. + frameDc = drawInfo.hdcDraw; + frameBounds = bounds; } - return result; + return renderFrame(); } LRESULT CMapWinGISControl::OnEraseBkgnd(